Skip to content

Commit 6cbdc0a

Browse files
committed
fix(token-guard): word-boundary match for sensitive env names (sync from template@aeac8c1)
1 parent b60e2e9 commit 6cbdc0a

2 files changed

Lines changed: 30 additions & 1 deletion

File tree

.claude/hooks/token-guard/index.mts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,19 @@ type ToolInput = {
120120
const hasRedaction = (command: string): boolean =>
121121
REDACTION_MARKERS.some(re => re.test(command))
122122

123+
// Word-boundary match so `PASS` doesn't fire on `PATHS-ALLOWLIST` and
124+
// `AUTH` doesn't fire on `AUTHOR`. Env-var-style boundaries treat `_`
125+
// as a separator (so `ACCESS_TOKEN` matches `TOKEN`) but require a
126+
// non-alphanumeric character on each end (so `PATHS` doesn't match
127+
// `PASS`). The pre-fix substring match created false positives
128+
// whenever a path name happened to contain a sensitive keyword as a
129+
// literal substring.
130+
const sensitiveEnvBoundaryRes = SENSITIVE_ENV_NAMES.map(
131+
frag => new RegExp(String.raw`(?:^|[^A-Z0-9])${frag}(?:[^A-Z0-9]|$)`),
132+
)
123133
const referencesSensitiveEnv = (command: string): boolean => {
124134
const upper = command.toUpperCase()
125-
return SENSITIVE_ENV_NAMES.some(frag => upper.includes(frag))
135+
return sensitiveEnvBoundaryRes.some(re => re.test(upper))
126136
}
127137

128138
const matchesAlwaysDangerous = (command: string): RegExp | null => {

.claude/hooks/token-guard/test/token-guard.test.mts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,25 @@ describe('token-guard hook', () => {
182182
})
183183
})
184184

185+
describe('does not false-positive on substring of sensitive name', () => {
186+
// Regression: `PATHS-ALLOWLIST.YML` toUpperCase()d contains `PASS`
187+
// as a substring, which the pre-fix unbounded match treated as
188+
// a sensitive env reference. Word-boundary fix means `PASS` must
189+
// be a standalone token (or at a `_`/`-`/`.`/`/` boundary).
190+
it('paths-allowlist.yml does not trip PASS', () => {
191+
assert.equal(runHook('cat .github/paths-allowlist.yml').code, 0)
192+
})
193+
it('AUTHOR_NAME does not trip AUTH', () => {
194+
// AUTHOR ends with R; the boundary-after match correctly skips
195+
// it because the next char is `_`, but `AUTH` followed by `O`
196+
// (alphanumeric) is not a token boundary.
197+
assert.equal(runHook('echo $AUTHOR_NAME').code, 0)
198+
})
199+
it('PASSAGE_TIME does not trip PASS', () => {
200+
assert.equal(runHook('echo $PASSAGE_TIME').code, 0)
201+
})
202+
})
203+
185204
describe('fails open on malformed input', () => {
186205
it('empty stdin', () => {
187206
const r = spawnSync(nodeBin, [hookScript], {

0 commit comments

Comments
 (0)