Skip to content

fix(hooks): normalize-ifs.mjs generalizes :+/+ whitespace bypass beyond IFS - #2585

Merged
carlos-alm merged 2 commits into
mainfrom
fix/issue-2558
Aug 19, 2026
Merged

fix(hooks): normalize-ifs.mjs generalizes :+/+ whitespace bypass beyond IFS#2585
carlos-alm merged 2 commits into
mainfrom
fix/issue-2558

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Found while fixing guard-git.sh: unquoted ${IFS}/whitespace-expansion between git/gh and any subcommand bypasses every check #2451 (Greptile review round 3 on PR fix(hooks): normalize $IFS/${IFS} before any guard-git.sh verb-detection check #2557): normalize-ifs.mjs correctly recognizes \${IFS:+ }/\${IFS+ } as producing a literal space, but this operator's behavior has nothing to do with the variable being named IFS specifically — bash's \${VAR:+word} substitutes word whenever VAR is set and non-null, regardless of VAR's own value. \${HOME:+ }, \${PWD:+ }, \${PATH:+ }, or any other normally-set variable works identically.
  • A per-variable-name normalizer can never fully close this class, since the variable name in the bypass isn't fixed — this issue's own text lays out two options: a bounded regex generalizing the shape regardless of name (what this PR does), or a much larger architectural rethink (actual shell parsing instead of static regex heuristics, tracked separately as a known, accepted broader limitation).
  • The other three replacements in this file (\${IFS}, \$IFS, and the substring-expansion form) stay IFS-specific, since they extract from IFS's own known default value (space/tab/newline) — that reasoning doesn't generalize to other variables, whose actual values aren't known statically. Only the :+/+ alternate-value replacement — whose substituted text is entirely independent of the named variable's value — now matches any bash identifier shape ([A-Za-z_][A-Za-z0-9_]*) in that position.
  • Kept docs/examples/claude-code-hooks/normalize-ifs.mjs byte-identical to .claude/hooks/normalize-ifs.mjs, as enforced by tests/unit/hook-guard-git-clean.test.ts.

Closes #2558

Test plan

  • New tests in tests/unit/hook-guard-git-ifs-bypass.test.ts: blocks git\${HOME:+ }reset and git\${PWD+ }reset (generalized bypass via non-IFS variables), does not flag \${SOME_VAR:+x} (non-whitespace substituted text, any name).
  • Revert-verify: temporarily reverted the regex to the IFS-only form and confirmed both new tests fail with exactly the pre-fix symptom (expected false to be true); restored the fix and all pass again.
  • Full guard-git test suite (hook-guard-git-ifs-bypass, hook-guard-git-clean, hook-guard-git-branch-validation, hook-guard-git-commit-cwd-fallback) — 82/82 pass, confirming the byte-identical-copies check still holds.
  • npm run lint clean.
  • Full npx vitest run: 5528/5528 tests pass (344 test files, +3 new).

…nd IFS

\${IFS:+ }/\${IFS+ } was matched by literal name, but bash's alternate-value
expansion substitutes its own "word" whenever the named variable is set and
non-null -- true for almost any commonly-set variable (HOME, PWD, PATH,
...), not just IFS. The substituted text has nothing to do with the named
variable's own value, so a per-variable-name check could never fully close
this class: an obfuscator can pick any variable known to be set in the
target shell. This one replacement (only) now matches any bash identifier
shape in that position, instead of the literal name IFS -- the other three
replacements stay IFS-specific since they extract from IFS's own known
default value, which doesn't generalize the same way.

Closes #2558

docs check acknowledged

Impact: 1 functions changed, 0 affected
@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR generalizes whitespace alternate-value normalization beyond IFS and completes the previous special-parameter fix.

  • Recognizes ordinary Bash identifiers, positional parameters, and special parameters in ${parameter:+ } and ${parameter+ }.
  • Keeps the live hook and documented example synchronized.
  • Adds regression coverage for ordinary variables and representative always-set special parameters.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
.claude/hooks/normalize-ifs.mjs Broadens alternate-value normalization to ordinary and special Bash parameters, fully covering the previously reported special-parameter bypass.
docs/examples/claude-code-hooks/normalize-ifs.mjs Mirrors the live normalizer changes to preserve the documented hook synchronization contract.
tests/unit/hook-guard-git-ifs-bypass.test.ts Adds regression tests for non-IFS variables and representative always-set Bash special parameters.

Reviews (2): Last reviewed commit: "fix: normalize-ifs.mjs :+/+ generalizati..." | Re-trigger Greptile

Comment thread .claude/hooks/normalize-ifs.mjs Outdated
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

1 functions changed0 callers affected across 0 files

  • event:end in docs/examples/claude-code-hooks/normalize-ifs.mjs:161 (0 transitive callers)

…ters too

An ordinary bash identifier isn't the only thing that can sit in the :+/+
alternate-value position -- special parameters ($?, $$, $#, $-, $!) and
positional parameters (${10:+ } etc.) use the same syntax and the same
always/normally-set semantics, but none of them match an identifier shape
([A-Za-z_][A-Za-z0-9_]*). Verified directly against real bash:
${?:+ }/${$:+ }/${#:+ }/${-:+ } all substitute the whitespace word exactly
like an ordinary variable would. The regex now matches an identifier, a
bare digit sequence, or one of ?$!#@*- in that position.

docs check acknowledged

Impact: 1 functions changed, 0 affected
@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
carlos-alm merged commit e3903ce into main Aug 19, 2026
55 of 59 checks passed
@carlos-alm
carlos-alm deleted the fix/issue-2558 branch August 19, 2026 06:40
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

guard-git.sh: any normally-set variable (not just IFS) can inject a literal whitespace token-boundary via ${VAR:+ } / ${VAR+ }

1 participant