hooks
Claude Code hooks (secret-guard.py also serves Codex). bin/link.sh / nix/home.nix symlink these into ~/.claude/hooks/; wiring is manual, see below.
| hook | event | what |
|---|---|---|
comms-lint.py |
PreToolUse / Bash |
Gates gh issue|pr create|edit|comment|review. Lints body against claude-md/writing.md (150-word target / 300 hard cap above fold, no reviewer-addressing opener, plain diction, ≤4 bold spans, no essay headings). Exit 2 blocks, stderr becomes feedback. |
comment-lint.py |
PostToolUse / Write|Edit|MultiEdit |
Lints newly-added comment lines in code files against claude-md/writing.md. Exit 2 = revise nudge (edit already applied). Long-comment-run finding (>3 lines) is advisory, delivered via additionalContext. |
secret-guard.py |
PreToolUse / Bash|Write|Edit|MultiEdit|NotebookEdit |
Blocks tool arguments carrying a live secret: any ~/.env.claude value of 6+ chars (value-based, so near-zero false positives; 6-7 char values match as standalone tokens) plus literal token shapes (ghp_, sk-, AKIA, private-key headers, credential-bearing URLs). Values under 6 chars are too short to guard — the hook emits a daily rotate warning for them instead. Exit 2 blocks; stderr names the variable, never the value. Also serves Codex via the same entry in ~/.codex/hooks.json. |
All fail open on anything they can't parse. Debug the linters with COMMS_LINT_DEBUG=1 / COMMENT_LINT_DEBUG=1.
Wiring
~/.claude/settings.json is machine-local (MCP servers, statusline, per-box hooks), so this repo does not own it. Merge into .hooks:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/comms-lint.py" }] },
{ "matcher": "Bash|Write|Edit|MultiEdit|NotebookEdit",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/secret-guard.py" }] }
],
"PostToolUse": [
{ "matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/comment-lint.py" }] }
]
}
}
Contract prose lives in claude-md/writing.md, linked to ~/.claude/writing.md and imported from ~/.claude/CLAUDE.md via @writing.md. Both linters cite that path in their block message — moving the fragment means updating the linter string too.
Testing a hook
Feed it the payload shape Claude Code sends:
echo '{"tool_name":"Bash","tool_input":{"command":"gh pr comment 1 --body \"short\""}}' \
| ~/.claude/hooks/comms-lint.py; echo "exit=$?"