claude-nomad 0.26.1 → 0.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.2](https://github.com/funkadelic/claude-nomad/compare/v0.26.1...v0.26.2) (2026-05-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
* **gitleaks:** condense push output and group doctor check-shared layout ([#161](https://github.com/funkadelic/claude-nomad/issues/161)) ([d9e5758](https://github.com/funkadelic/claude-nomad/commit/d9e57589f616e70b8e2d6249c957af55a25a578b))
|
|
9
|
+
|
|
3
10
|
## [0.26.1](https://github.com/funkadelic/claude-nomad/compare/v0.26.0...v0.26.1) (2026-05-27)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* Owns the post-stage block: run the shared `scanStagedTree` (the same git
|
|
8
8
|
* init + add + `gitleaks protect --staged` mechanism push uses), classify the
|
|
9
9
|
* findings via `partitionFindings`, and emit the doctor glyph rows (clean,
|
|
10
|
-
* per-session leak
|
|
11
|
-
*
|
|
10
|
+
* per-session leak rows, then a Remediation block, then a Finding types legend).
|
|
11
|
+
* All external work flows through `scanStagedTree`; this module spawns
|
|
12
12
|
* nothing itself.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
|
|
17
|
-
import { green, red, dim, okGlyph, failGlyph } from './color.ts';
|
|
17
|
+
import { green, red, dim, bold, okGlyph, failGlyph } from './color.ts';
|
|
18
18
|
import { addItem, type DoctorSection } from './commands.doctor.format.ts';
|
|
19
19
|
import { CLAUDE_HOME } from './config.ts';
|
|
20
20
|
import { type Finding, partitionFindings, scanStagedTree } from './push-gitleaks.ts';
|
|
@@ -32,31 +32,18 @@ function scrubPath(logical: string, sid: string, logicalToEncoded: Map<string, s
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Emit one fail row per affected session
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* rather than print a wrong path if the invariant ever breaks; the leak row is
|
|
40
|
-
* always emitted.
|
|
35
|
+
* Emit one fail row per affected session (the `✗ ... in session` rows only)
|
|
36
|
+
* and set `process.exitCode = 1`. Rotate-and-scrub guidance and the
|
|
37
|
+
* false-positive hint are NOT emitted here; they belong to `reportRemediation`
|
|
38
|
+
* which is called after all finding rows.
|
|
41
39
|
*/
|
|
42
40
|
function reportSessionFindings(
|
|
43
41
|
section: DoctorSection,
|
|
44
42
|
bySession: Map<string, Map<string, number>>,
|
|
45
|
-
logicalBySession: Map<string, string>,
|
|
46
|
-
logicalToEncoded: Map<string, string>,
|
|
47
43
|
): void {
|
|
48
44
|
for (const [sid, counts] of bySession) {
|
|
49
45
|
const summary = [...counts.entries()].map(([rule, n]) => `${rule} (${n})`).join(', ');
|
|
50
46
|
addItem(section, `${red(failGlyph)} ${red(summary)} in session ${sid}`);
|
|
51
|
-
const logical = logicalBySession.get(sid);
|
|
52
|
-
/* c8 ignore next -- false branch is defensive; every bySession sid is keyed in logicalBySession */
|
|
53
|
-
if (logical !== undefined) {
|
|
54
|
-
addItem(
|
|
55
|
-
section,
|
|
56
|
-
` ${dim(`rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`)}`,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
addItem(section, ` ${dim('false positive? add a pattern to .gitleaks.toml')}`);
|
|
60
47
|
}
|
|
61
48
|
process.exitCode = 1;
|
|
62
49
|
}
|
|
@@ -75,6 +62,36 @@ function reportOtherFindings(section: DoctorSection, other: Finding[]): void {
|
|
|
75
62
|
process.exitCode = 1;
|
|
76
63
|
}
|
|
77
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Emit the remediation block after all finding rows. Findings-first ordering
|
|
67
|
+
* means guidance is grouped at the end, not interleaved between session rows.
|
|
68
|
+
* Emits a leading blank, a bold `Remediation` header, one rotate-and-scrub
|
|
69
|
+
* line per session (using `logicalBySession` to build the scrub path), and
|
|
70
|
+
* exactly ONE false-positive hint after the loop (deduped, not once per session).
|
|
71
|
+
* The hint guard omits a rotate row rather than print a wrong path if the
|
|
72
|
+
* invariant ever breaks; the false-positive line is always appended.
|
|
73
|
+
*/
|
|
74
|
+
function reportRemediation(
|
|
75
|
+
section: DoctorSection,
|
|
76
|
+
bySession: Map<string, Map<string, number>>,
|
|
77
|
+
logicalBySession: Map<string, string>,
|
|
78
|
+
logicalToEncoded: Map<string, string>,
|
|
79
|
+
): void {
|
|
80
|
+
addItem(section, '');
|
|
81
|
+
addItem(section, bold('Remediation'));
|
|
82
|
+
for (const [sid] of bySession) {
|
|
83
|
+
const logical = logicalBySession.get(sid);
|
|
84
|
+
/* c8 ignore next -- false branch is defensive; every bySession sid is keyed in logicalBySession */
|
|
85
|
+
if (logical !== undefined) {
|
|
86
|
+
addItem(
|
|
87
|
+
section,
|
|
88
|
+
` ${dim(`- rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`)}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
addItem(section, ` ${dim('- false positive? add a pattern to .gitleaks.toml')}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
/**
|
|
79
96
|
* Captures both the `<logical>` segment and the `<sid>` from a repo-relative
|
|
80
97
|
* `shared/projects/<logical>/<sid>.jsonl` path. The session-id group matches
|
|
@@ -112,14 +129,14 @@ function buildLogicalBySession(findings: Finding[]): Map<string, string> {
|
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
/**
|
|
115
|
-
* Emit a deduplicated description legend in the footer: one
|
|
116
|
-
* description` row per distinct RuleID across all findings,
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* (older gitleaks, custom rules) are skipped, and the whole block
|
|
120
|
-
* the surrounding
|
|
121
|
-
* legend lives in the footer so a rule hit across many files
|
|
122
|
-
* (e.g. `sonar-api-token`) is explained once, not per occurrence.
|
|
132
|
+
* Emit a deduplicated description legend in the footer: one `- [rule-id]:
|
|
133
|
+
* description` row per distinct RuleID across all findings, headed by a bold
|
|
134
|
+
* `Finding types` header with a leading blank but no trailing blank (renderSection
|
|
135
|
+
* attaches the `└` elbow to the last non-empty item). Rules whose description
|
|
136
|
+
* is absent (older gitleaks, custom rules) are skipped, and the whole block
|
|
137
|
+
* (including the surrounding blank and header) is omitted when no descriptions
|
|
138
|
+
* are available. The legend lives in the footer so a rule hit across many files
|
|
139
|
+
* or sessions (e.g. `sonar-api-token`) is explained once, not per occurrence.
|
|
123
140
|
*/
|
|
124
141
|
function emitDescriptionLegend(section: DoctorSection, findings: Finding[]): void {
|
|
125
142
|
const descByRule = new Map<string, string>();
|
|
@@ -128,16 +145,19 @@ function emitDescriptionLegend(section: DoctorSection, findings: Finding[]): voi
|
|
|
128
145
|
}
|
|
129
146
|
if (descByRule.size === 0) return;
|
|
130
147
|
addItem(section, '');
|
|
148
|
+
addItem(section, bold('Finding types'));
|
|
131
149
|
for (const [rule, desc] of descByRule) {
|
|
132
|
-
addItem(section, ` ${red(
|
|
150
|
+
addItem(section, ` ${red(`- [${rule}]`)}: ${dim(desc)}`);
|
|
133
151
|
}
|
|
134
|
-
addItem(section, '');
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
/**
|
|
138
155
|
* Scan the staged temp tree through the shared `scanStagedTree` and emit the
|
|
139
|
-
* result rows
|
|
140
|
-
*
|
|
156
|
+
* result rows in findings-first order: other-bucket leak rows, then per-session
|
|
157
|
+
* leak rows, then a `Remediation` block (gated on bySession.size > 0), then a
|
|
158
|
+
* `Finding types` legend (gated on at least one finding carrying a Description).
|
|
159
|
+
* Isolates the deepest nesting from `reportCheckShared`: the scan try/catch
|
|
160
|
+
* (failure -> fail row + exit 1, carrying `err.message` only, never
|
|
141
161
|
* stderr/stdout), the unparseable `findings === null` branch, `partitionFindings`,
|
|
142
162
|
* and the clean / `other` / `bySession` rows. BOTH buckets gate the clean row: a
|
|
143
163
|
* finding in `other` (nested transcripts matching neither the flat `SESSION_PATH`
|
|
@@ -175,8 +195,9 @@ export function scanAndReport(
|
|
|
175
195
|
return;
|
|
176
196
|
}
|
|
177
197
|
if (other.length > 0) reportOtherFindings(section, other);
|
|
198
|
+
if (bySession.size > 0) reportSessionFindings(section, bySession);
|
|
178
199
|
if (bySession.size > 0) {
|
|
179
|
-
|
|
200
|
+
reportRemediation(section, bySession, buildLogicalBySession(findings), logicalToEncoded);
|
|
180
201
|
}
|
|
181
202
|
emitDescriptionLegend(section, findings);
|
|
182
203
|
}
|
|
@@ -83,10 +83,12 @@ export function readGitleaksReport(reportPath: string): Finding[] | null {
|
|
|
83
83
|
*
|
|
84
84
|
* `forwardStreams` (default `false`): when `true`, the gitleaks redacted
|
|
85
85
|
* stderr/stdout captured on a non-zero exit is written to the process streams
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* the
|
|
86
|
+
* ONLY on the scan-crash path (when the report is unparseable or missing, i.e.
|
|
87
|
+
* `readGitleaksReport` returns `null`). On the leaks-found path the report
|
|
88
|
+
* parses to a findings array, the structured caller FATAL fully describes the
|
|
89
|
+
* findings, and the raw streams are suppressed to avoid printing them twice.
|
|
90
|
+
* `runGitleaksScan` passes `true`; the read-only `--check-shared` preflight
|
|
91
|
+
* leaves it `false` so it never writes to streams on any path.
|
|
90
92
|
*/
|
|
91
93
|
export function scanStagedTree(repoDir: string, forwardStreams = false): Finding[] | null {
|
|
92
94
|
const cacheDir = join(homedir(), '.cache', 'claude-nomad');
|
|
@@ -111,11 +113,12 @@ export function scanStagedTree(repoDir: string, forwardStreams = false): Finding
|
|
|
111
113
|
} catch (err) {
|
|
112
114
|
const e = err as NodeJS.ErrnoException & { stderr?: Buffer; stdout?: Buffer };
|
|
113
115
|
if (e.code === 'ENOENT') throw err;
|
|
114
|
-
|
|
116
|
+
const report = readGitleaksReport(reportPath);
|
|
117
|
+
if (forwardStreams && report === null) {
|
|
115
118
|
if (e.stderr) process.stderr.write(e.stderr);
|
|
116
119
|
if (e.stdout) process.stdout.write(e.stdout);
|
|
117
120
|
}
|
|
118
|
-
return
|
|
121
|
+
return report;
|
|
119
122
|
} finally {
|
|
120
123
|
rmSync(reportPath, { force: true });
|
|
121
124
|
}
|
package/src/push-gitleaks.ts
CHANGED
|
@@ -92,6 +92,20 @@ export function partitionFindings(findings: Finding[]): {
|
|
|
92
92
|
* session, since those nested paths route to the `other` bucket and are
|
|
93
93
|
* not listed per-session. Pure.
|
|
94
94
|
*/
|
|
95
|
+
/**
|
|
96
|
+
* Render one `Also found:` row for a non-session ("other"-bucket) finding as
|
|
97
|
+
* ` <File>:<StartLine> <RuleID>`, where the line number is the manual-scrub
|
|
98
|
+
* locator for the nested transcript. `StartLine` is typed `number` but comes
|
|
99
|
+
* from an unvalidated `parsed as Finding[]` cast over gitleaks subprocess
|
|
100
|
+
* output, so a missing or non-positive value (gitleaks line numbers are
|
|
101
|
+
* 1-indexed) drops the `:<line>` suffix rather than emit a confusing
|
|
102
|
+
* `:undefined` / `:0`.
|
|
103
|
+
*/
|
|
104
|
+
export function formatOtherFinding(f: Finding): string {
|
|
105
|
+
const loc = Number.isInteger(f.StartLine) && f.StartLine > 0 ? `:${f.StartLine}` : '';
|
|
106
|
+
return ` ${f.File}${loc} ${f.RuleID}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
export function buildSessionAwareFatal(
|
|
96
110
|
bySession: Map<string, Map<string, number>>,
|
|
97
111
|
other: Finding[],
|
|
@@ -110,7 +124,7 @@ export function buildSessionAwareFatal(
|
|
|
110
124
|
lines.push(
|
|
111
125
|
'',
|
|
112
126
|
'Also found:',
|
|
113
|
-
...other.map(
|
|
127
|
+
...other.map(formatOtherFinding),
|
|
114
128
|
' Review with: git diff --cached, then unstage manually.',
|
|
115
129
|
);
|
|
116
130
|
}
|
|
@@ -133,8 +147,10 @@ export function buildSessionAwareFatal(
|
|
|
133
147
|
* missing/locked file, or a non-finding runtime failure, since gitleaks v8.x
|
|
134
148
|
* returns exit 1 for both "leaks found" and runtime errors) it throws a
|
|
135
149
|
* distinct scan-failed FATAL so the operator does not chase a phantom
|
|
136
|
-
* `nomad drop-session` recovery
|
|
137
|
-
*
|
|
150
|
+
* `nomad drop-session` recovery. On the leaks-found path the raw gitleaks
|
|
151
|
+
* streams are suppressed (the session-aware FATAL fully describes the findings).
|
|
152
|
+
* On the scan-failed/null-report path the raw stderr/stdout is forwarded so
|
|
153
|
+
* "Review the gitleaks output above." has something to point at.
|
|
138
154
|
*
|
|
139
155
|
* ENOENT (gitleaks or git absent) propagates from the helper and is mapped to
|
|
140
156
|
* the platform-aware install-hint FATAL. Defense-in-depth: the presence probe
|