Skip to content

feat(hooks): enforcement hooks — PreToolUse git gate + SessionStart brief (3.2.0) - #33

Merged
ashmitb95 merged 16 commits into
mainfrom
feat/enforcement-hooks
Jul 4, 2026
Merged

feat(hooks): enforcement hooks — PreToolUse git gate + SessionStart brief (3.2.0)#33
ashmitb95 merged 16 commits into
mainfrom
feat/enforcement-hooks

Conversation

@ashmitb95

Copy link
Copy Markdown
Owner

What

Canopy 4.0 phase 2: Claude Code enforcement hooks that make the two core mistakes impossible rather than merely discouraged, installed by canopy setup-agent --hooks into project-scoped .claude/settings.json.

Motivated by mining 35 days of real work-machine transcripts (135 git errors / 111 midway branch switches / 82 wrong-path incidents). See ~/.claude/plans/canopy-4.0-distillation.md#evidence.

PreToolUse git gate (canopy-hook-gate)

Parses each Bash command into per-segment effective directories (resolving cd chains, git -C, and heredoc bodies — the agent's cwd never leaves the workspace parent, so raw-cwd checks would false-positive on ~357 legitimate cd x && git … chains). Blocks git mutations that:

  • run outside any workspace repo/slot worktree — outside_repo
  • commit/push on a branch registered to a different feature than canonical — trunk_branch_drift / slot_branch_drift
  • push a branch name that only exists in another repo — push_unknown_branch (the real src refspec … does not match any failure)

Fail-open by design: unparseable commands, unresolvable cd $VAR, no canopy.toml, or any internal error → allow. CANOPY_HOOKS_DISABLED=1 disables entirely. git checkout/switch are never blocked (they're the recovery action).

SessionStart brief (canopy-hook-context)

Injects a compact repo → branch (dirty) · canonical feature · slots map at session start so a wrong-branch session is visible before the agent reads a file.

Correctness

  • Parser validated against a 680-shape corpus of real mined commands (tests/fixtures/hook_gate_corpus.jsonl) — regression test asserts no crashes + ≥90% mutation detection (96.8% on true mutations; the rest are legit fail-open skips like git-inside-subshell).
  • Three adversarial review rounds on the gate parser (heredoc edge cases, push-redirect false-positives, -c/-C interaction, cd-flag resolution) + one on the installer (malformed-settings shape-safety, atomic write, --check reporting).
  • Installer is non-destructive, idempotent, refuses (never clobbers) a settings.json it doesn't recognize, and writes atomically.

Tests

942 passed. ~110 hook-specific tests across test_hook_gate, test_hook_gate_corpus, test_hook_context, test_install_hooks, test_hooks_entry.

Version

3.2.0 (CHANGELOG updated). Fast-path latency ~6ms import / ~50ms end-to-end; the gate only loads a workspace when git appears in the command.

Not in this PR (deferred)

Manual multi-chat dogfood in canopy-test → deferred to post-4.0.0 per project decision. Automated replay harness (drives the real binary through mined scenarios) planned before phase 4.

ashmitb95 added 16 commits July 4, 2026 09:18
Adds MUTATION_SUBCOMMANDS + is_mutation(seg) to hook_gate, plus corpus
regression tests over 680 real command shapes. No parser changes were
needed: detection rate is 255/263 (97.0%). The 8 misses are legitimate
fail-open skips (6 heredoc commit bodies unparseable by shlex, 2 git
strings embedded in quoted echo args piped to another script).
…flag resolution, newline splitting, stash reads

- push refspec check: scan positionals properly — stop at redirects
  (2>&1, > file, <) and background &, skip values of -o/--push-option/
  --repo/--receive-pack/--exec, so shell trailers are never treated as
  refspecs (was a false block on `git push origin main 2>&1`)
- resolve_segments: `git -c key=val -C dir` now resolves the -C dir
  (the -c value token no longer aborts the global-flag scan)
- cd branch: skip leading -P/-L/-e/-- option tokens before taking the
  target; flag-like or missing target marks dir unknown (fail open)
- split_top_level: unquoted depth-0 newlines split like `;`, so
  `cd api\ngit commit` chains are gated (corpus rate held at 255/263)
- is_mutation: `git stash list`/`stash show` are reads; only bare stash
  and push/pop/apply/drop/clear/save/branch sub-verbs gate
- trunk-drift deny reason: no longer suggests `canopy switch None` when
  no feature is canonical
- style: re import at module top, dead quote-strip removed from
  _resolve_path, gate_command docstring corrected, redaction-guard
  comment added; new test covers per-repo branches map in owner lookup
…ations

- split_top_level: unquoted depth-0 <<DELIM (<<EOF, <<-EOF, <<'EOF',
  <<"EOF"; <<< herestrings ignored) suppresses ALL splitting — newlines
  and operators — until the line matching DELIM, then resumes. Fixes two
  round-1 regressions from newline splitting: a heredoc body that merely
  mentions `git push` was blocked outside_repo, and a body `cd` line
  poisoned the tracked dir for a real mutation after the heredoc. An
  unterminated heredoc leaves the remainder as one segment (fails open).
- is_mutation: a flag right after `stash` (`stash -u`, `--keep-index`,
  `--include-untracked`) is an implicit `stash push` — gated as a
  mutation; `stash list`/`show` stay reads.

Corpus: heredoc change loses zero shapes; stash-flag fix detects one
more (242/263 tagged; the 13 stash-read shapes were intentionally
reclassified by round 1's fix 5, remaining 8 misses pre-date round 1).
…ors, herestring guard

- _heredoc_delimiter: accept the POSIX backslash-quoted form <<\EOF
  (equivalent to <<'EOF') so its body is swallowed instead of
  newline-split (was a verified false block on a body mentioning
  `git push`)
- terminator matching: plain << now requires the line to equal the
  delimiter EXACTLY (an indented `  EOF` or trailing-space `EOF ` line
  no longer ends the heredoc early and leaks body lines as gated
  segments); <<- strips leading tabs only, per shell semantics
- herestring guard: also bail when the char before << is `<`, so the
  scan cannot re-enter <<< at its middle `<` and open a bogus heredoc
  that swallowed all subsequent real commands (detection regression,
  fail-open direction)

Corpus rate unchanged at 242/263.
…-check/--hooks status, no-workspace skip, numeric slot sort, shim tests
@ashmitb95
ashmitb95 merged commit e740dea into main Jul 4, 2026
3 checks passed
@ashmitb95
ashmitb95 deleted the feat/enforcement-hooks branch July 4, 2026 05:30
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