Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,53 @@ See `~/.claude/templates/DECISIONS.md` for the ADR template.
---

<!-- Add new ADRs below this line -->

## ADR-001: Claude Code conditional hooks (`if` field)

**Date:** 2026-03-30
**Status:** Applied
**Ticket:** SAM-218

### Context

Claude Code v2.1.85+ supports an `if` field on hook handlers that uses permission rule syntax (e.g., `Bash(git *)`) to pre-filter which tool calls spawn the hook process. Without it, every Bash call spawns all Bash-matcher hook processes — they run and exit immediately if the command doesn't match, wasting startup overhead.

### Decision

Apply `if` field filtering to Bash hooks that do internal command matching. Move the filtering to the Claude Code runtime level so hook processes only spawn when they have work to do.

### Changes applied to `~/.claude/settings.json`

| Hook | `if` condition | Before |
|------|----------------|--------|
| `conventional_commits.py` | `Bash(git commit *)` | spawned on every Bash call |
| `tsc_precommit.py` | `Bash(git commit *)` | spawned on every Bash call |
| `git_safety.py` | `Bash(git *)` | spawned on every Bash call |
| `pnpm_enforcer.py` | `Bash(npm *)` + `Bash(yarn *)` (two entries) | spawned on every Bash call |
| `trauma_guard.py` | none (unchanged) | dynamic JSONL patterns — must run on all Bash |

### Corresponding script simplifications

- `conventional_commits.py`: removed `if 'git commit' not in cmd: sys.exit(0)` (now pre-filtered)
- `tsc_precommit.py`: removed `if "git commit" not in cmd: sys.exit(0)` (now pre-filtered)
- `pnpm_enforcer.py`: removed `re.search(npm|yarn)` early-exit guard (now pre-filtered by two `if` entries)

### Hooks intentionally excluded

- `writing_guard.py`: auto-generated by `margin export profile` CLI; script changes would be overwritten on next export. The `Write|Edit` matcher would need to be restructured into per-extension handlers (10+ entries for `.md`, `.mdx`, `.txt`, `.html`, `.htm` × Write + Edit). Marginal gain vs complexity; deferred.
- `voice_loader.py`: same Write|Edit matcher restructuring required. Excluded for now.
- `trauma_guard.py`: patterns loaded at runtime from `~/.cass-memory/traumas.jsonl`; `if` field cannot express dynamic conditions.

### Validation

All hooks pass functional tests post-change:
- `conventional_commits.py`: blocks bad commit messages, passes good ones, exits silently on non-commit input
- `tsc_precommit.py`: syntax valid; only runs when `if` pre-filter passes
- `git_safety.py`: blocks `git push --force`, exits silently on non-git Bash
- `pnpm_enforcer.py`: blocks `npm install` in pnpm project, exits silently on git/other commands

### Notes

- `Bash(npm *)` catches all npm commands (including `npm --version`), not just install/add/etc. Wider than the previous subcommand filter, but acceptable — all npm use in pnpm projects should use pnpm.
- Multiple `if` values on one handler are not supported; OR conditions require separate handler entries (hence two entries for pnpm_enforcer).
- Glob brace expansion for Write/Edit (`Edit(*.{md,mdx})`) was not explored — deferred with writing hooks.
Loading