Skip to content

fix(hook): catch two spellings that took the guards away unnoticed - #33

Merged
rmyndharis merged 1 commit into
mainfrom
fix/hook-bypass-spellings
Aug 12, 2026
Merged

fix(hook): catch two spellings that took the guards away unnoticed#33
rmyndharis merged 1 commit into
mainfrom
fix/hook-bypass-spellings

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Two ways a command could remove the managed guards without the agent layer
reading it as a bypass.

--config-env= did not set the hooks-path flag

parseGit handles four spellings of a config override: -c key=value,
-ckey=value, --config-env key=value, and --config-env=key=value. The first
three run all three predicates. The fourth ran hookAffectingConfig and
aliasAffectingConfig but not hooksPathOverrideConfig, so
commandHooksPathOverride stayed false.

A commit carrying git --config-env=core.hooksPath=EVIL commit -m x reached
history with the agent guard allowing it, and the advisory printed alongside
named --no-verify as the cause, which was not present.

The gap predates the module split — git show 013da61^:src/hook.mjs has the
same asymmetry — so it has never been covered. The fix is the one missing line,
matching the shape of the three branches beside it. Collapsing the four branches
into a shared helper was considered and dropped: they differ in slice offset and
in how far they advance, so merging them touches the hottest path of the parser
for no behaviour change.

The class test at tests/hook.test.mjs:3016 claims in its own comment to assert
the whole class and listed six forms. It now lists ten: both --config-env
spellings against core.hooksPath and against include.path. Only the inline
core.hooksPath form was leaking; the other three already denied, and pinning
them stops the set drifting apart again.

The hooks directory did not have to be spelled in full

commandMayTouchHooks tested for the literal string .git/hooks, so
rm -rf .git/h* read as an ordinary prefix and one commit on that line went
through with only the advisory path firing.

The test now matches a .git/h path segment. A bare segment match would deny
cat .git/HEAD && git commit, since the test is case-insensitive, so a
lookahead steps around HEAD. --git-path hooks is covered too, for the
rm -rf $(git rev-parse --git-path hooks) shape.

A deletion routed through find -delete, or a hook installer that replaces the
directory, still escapes a text test — no string on the command line names the
hooks. That is a limit of reading commands rather than watching the filesystem,
and the answer belongs at the enforcement layer, not here. The next invocation
already denies with "run 'aimhooman init' and retry", so exactly one commit
slips and the breakage is reported rather than silent.

The allow-by-default for unmodelled prefixes stays as it is. Refusing them
taught agents to drop the && gate rather than run the command separately,
which is why it was relaxed, and inverting it to an allowlist breaks
./scripts/check.sh && git commit and turns off the ref-mutation deny for every
build runner.

Tests

Two new denials and two new guardrails, added to the existing prefix tests:
.git/h* and the --git-path hooks form must deny; cat .git/HEAD and
git rev-parse HEAD~1 before a commit must stay allowed.

Full suite green.

The inline `--config-env=core.hooksPath=...` form set the bypass flag but never
the hooks-path override flag, unlike the three sibling branches beside it. The
gap predates the module split. A commit carrying that spelling passed the agent
guard, and the advisory it printed named `--no-verify` as the cause, which was
not present. The class test claimed in its own comment to assert the whole set
and listed six forms; it now lists ten.

The hooks directory also did not have to be spelled in full to be removed:
`commandMayTouchHooks` tested for the literal `.git/hooks`, so `rm -rf .git/h*`
read as an ordinary prefix. The test now matches the path segment, with a
lookahead that keeps `cat .git/HEAD && git commit` allowed, and covers
`--git-path hooks`. A deletion routed through `find` or a hook installer still
escapes a text test; that belongs to the enforcement layer, not the parser.
@rmyndharis rmyndharis changed the title Catch two spellings that took the guards away unnoticed fix(hook): catch two spellings that took the guards away unnoticed Aug 12, 2026
@rmyndharis
rmyndharis merged commit 638c4fd into main Aug 12, 2026
14 of 16 checks passed
@rmyndharis
rmyndharis deleted the fix/hook-bypass-spellings branch August 12, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant