Files
agent-skills/hooks
naps62 08195c1cd4 feat(hooks): track writing contracts + linters in repo
comms-lint.py and comment-lint.py lived only in ~/.claude, which is not
a git repo. Contract prose lived in ~/.claude/CLAUDE.md. Neither survived
a machine rebuild.

Prose moves to claude-md/ fragments, imported via @name.md. Linters move
to hooks/. link.sh and home.nix distribute both. settings.json wiring
stays manual — it holds machine-local MCP/statusline config this repo
must not own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaGR6ERGCfzWTtuv2rebGe
2026-07-27 14:34:29 +00:00
..

hooks

Claude Code hooks. Claude-only — Codex ignores. 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/public-comms.md (BLUF, ≤300 words above fold, ≤4 bold spans, no essay headings, table for 3+ options). Exit 2 blocks, stderr becomes feedback.
comment-lint.py PostToolUse / Write|Edit|MultiEdit Lints newly-added comment lines in code files against claude-md/code-comments.md. Exit 2 = revise nudge (edit already applied). Long-comment-run finding is advisory, delivered via additionalContext.

Both fail open on anything they can't parse. Debug 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" }] }
    ],
    "PostToolUse": [
      { "matcher": "Write|Edit|MultiEdit",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/comment-lint.py" }] }
    ]
  }
}

Contract prose lives in claude-md/, linked to ~/.claude/<name>.md and imported from ~/.claude/CLAUDE.md via @public-comms.md / @code-comments.md. Both linters cite those paths in their block message — moving a 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=$?"