boshu2/agentops-cc-hooks
Configure default Claude Code enforcement hooks and opt-in guard recipes. Triggers: "cc-hooks", "configure Claude Code hooks", "install hooks".
This is a copy. The original lives at boshu2/cc-hooks.
npx skills add https://github.com/boshu2/agentops --skill cc-hooks
Shell commands that fire at specific points in Claude Code's lifecycle.
Hooks enforce mechanically what prose cannot: a model can reason its way past
an instruction, but it cannot reason its way past an exit 2 — which is exactly
why every hook must be narrow, silent, and reversible.
Named failure mode — chatty happy path: a hook that emits stdout on exit 0
corrupts the tool call it was guarding; silence on success is part of the
contract, not a style preference.
hooks/hooks.json; skill copies and checkouts wire with one command (scripts/install-hooks.sh). Operators can disable per host (/plugin disable, or remove the settings matchers).stop_hook_active and scope matchers narrowly to prevent recursion and unrelated-command interception.<!-- TOC: Quick Start | Events | Blocking | Writing Hooks | Anti-Patterns | References -->
Add to ~/.claude/settings.json (user) or .claude/settings.json (project):
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"my-validator.sh"}]}]}}
| Event | When | Blocks? | Common Use |
|-------|------|---------|------------|
| PreToolUse | Before tool runs | Yes | Block/modify commands |
| PostToolUse | After tool succeeds | Feedback | Auto-format, lint |
| PermissionRequest | Permission dialog | Yes | Auto-approve/deny |
| UserPromptSubmit | Prompt submitted | Yes | Add context, validate |
| Stop | Claude finishes | Yes | Force continue |
| SessionStart | Session begins | No | Load context, set env |
| Notification | Notifications | No | Desktop alerts |
Full schemas: HOOK-EVENTS.md
"Bash" → exact match
"Edit|Write" → regex OR
"mcp__.*__write" → MCP tools
"*" or "" → all tools
Tools: Bash, Read, Write, Edit, Glob, Grep, Task, WebFetch, WebSearch
| Code | Effect |
|------|--------|
| 0 | Success - JSON parsed from stdout |
| 2 | Block - stderr fed to Claude |
| Other | Non-blocking error |
Simple (exit 2):
echo "Blocked: reason" >&2 && exit 2
JSON (exit 0):
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked"}}
Decisions: "allow" (auto-approve), "deny" (block), "ask" (show dialog)
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow",
"updatedInput":{"command":"modified-command"}}}
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[
{"type":"command","command":"dcg"},
{"type":"command","command":"rch"}
]}]}}
git reset --hard, rm -rf, git push --forceDetails: DCG-RCH.md
A copy-paste PreToolUse recipe that nudges agents to **load the coordination
skill before hand-rolling the am/atm/ntm/tmux send-keys CLI**. This
recipe auto-installs nothing; you opt in per host (unlike the policy
dispatcher, which ships by default).
Context-budget doctrine for hooks: hooks are the most powerful enforcement
(mechanical, can't be reasoned past) but they pollute context — use sparingly. A
hook must be SILENT on the happy path (exit 0, no stdout/stderr), fire ONLY on a
real violation (ideally once per session, sentinel-gated), prefer PreToolUse
violation-guards over UserPromptSubmit/SessionStart per-turn injectors, and
NEVER emit stray stdout on an exit-0 PreToolUse path (it is parsed as JSON and
breaks the tool call). Block via exit 2 + stderr.
The recipe ships both scripts verbatim, a precise head-only matcher (so a
br create --body "...am/atm/ntm..." never false-fires), the two-matcher
opt-in settings.json snippet, and a bats test proving every fire/silent case.
Recipe: SKILL-FIRST-COORDINATION-GUARD.md
A PreToolUse Edit|Write guard that routes an edit of an installed skill copy
(*/.claude/skills/**, .codex, .gemini) back to the repo source of truth
skills/<name>/. This is a TRUE mistake-token — editing an installed/symlinked
copy has no legitimate form (overwritten on install, or symlinks through to the
factory checkout). Zero false-positive surface: it matches tool_input.file_path
only, so a doc that merely mentions claude/skills in its body never fires.
Reversible → it ROUTES (exit 2 + one-line redirect), not hard-blocks. Silent on
every other path; fires once per session. Ships INERT — opt-in installer:
scripts/install-installed-skill-edit-guard.sh # user scope; --project for project
Recipe: INSTALLED-SKILL-EDIT-GUARD.md
The keystone guard ships gate-blind per-fire telemetry: on each fire it
appends exactly one JSONL line — {ts, session, token_class, path_sha256} — to
${AGENTOPS_HOME:-~/.agents/ao}/guardrail-telemetry.jsonl (override with
AGENTOPS_GUARDRAIL_TELEMETRY). The path is SHA-256 hashed, never raw
(privacy); nothing is written on the happy path; the sensor is inert until the
guard is installed and fires. The pre-registered methodology — metric =
declining fire-ATTEMPT rate over time (a signal the redirect cannot fake, NOT the
circular hand-roll rate), minimum N, noise floor, and **null-at-small-N is an
acceptable outcome** — satisfies ADR-0002 l.58 ("test or eval evidence showing
positive value"), the criterion whose absence killed 2.x hooks (#511).
Methodology: GUARDRAIL-VALUE-PROOF.md
The admission-control layer (epic age-4qw1): one PreToolUse dispatcher —
hooks/policy-dispatch.sh — evaluating a
policies-as-data registry
(policies/policies.json, contract
schemas/hooks-manifest.v2.schema.json) instead of N hand-wired settings
entries. This is the membrane at tool-call altitude: same vocabulary, lower
altitude than the pawl/gate at push time.
Per policy: dcg-style id (domain.object:token), mode: deny | route | audit,
matchers (tool + command/file_path regex), a route_message that names THE
correct tool, a rationale, and a pre-registered value_proof (the ADR-0002
lease-on-life: no proof accruing → retire the policy).
Predicate discipline, schema-enforced (the #511 anti-lesson): only
predicate_class: pure — syntactic mistake-tokens over the command or file
path — may deny/route. Lookup/stateful predicates ship audit-only until
promoted with reviewed fires.
scripts/lint-policies.sh enforces this mechanically
(jq-only; runs in bats and CI).
Accepted false-positive surface: because a pure predicate matches its token
anywhere in the raw command string, a protected token quoted as *data* (a commit
message body, a dcg test "..." probe, a here-doc payload) can still fire even
though nothing harmful would run. This is the deliberate cost of the
pure-only-may-deny rule — the alternative (repo/context lookups) is exactly the
stateful predicate the discipline bars from deny. Every fire is reversible: a
one-shot AOP_WAIVE=<policy-id> or a policy-waivers line clears it.
Semantics: happy path = exit 0, zero output. deny = exit 2 + one stderr
route line (full message once per session, short line after — every attempt
still blocks). route = exit 0 + permissionDecision:"ask" JSON. audit =
allow + record. Every fire appends one hashed guardrail-telemetry line
(token_class = policy id, plus mode/decision). Waive once with
AOP_WAIVE=<policy-id>, or a policy-waivers file line
<policy-id> <expiry-epoch>. Missing registry or jq fails OPEN.
Day-1 enforce cohort (age-wnyt, all pure-regex, high-pain):
| Policy | Blocks | Routes to |
|---|---|---|
| core.git:add-beads-ledger | git add naming _beads/ (private ledger leak is one-way) | push the ledger repo itself — never git add _beads in the public tree |
| core.provenance:ledger-hand-append | redirect/tee/Edit/Write onto docs/provenance/ledger.jsonl (hash-chained, sealed) | ao provenance add |
| core.skills:copy-into-installed | cp/rsync/mv INTO ~/.claude|.codex|.gemini/skills (dest-position enforced) | ao skills link |
| core.skills:edit-installed-copy | Edit/Write of an installed skill copy (file_path only — prose can never fire it) | edit repo skills/<name>/ |
How it reaches users — every install path delivers hooks:
| Install path | Delivery |
|---|---|
| Claude Code plugin (claude plugin install agentops@agentops-marketplace) | Automatic — the plugin bundles hooks/hooks.json (${CLAUDE_PLUGIN_ROOT} paths); hooks are active on install, no wiring step |
| npx skills@latest add boshu2/agentops / skills.sh copy | The skill package carries its own installer: ~/.claude/skills/cc-hooks/scripts/install-hooks.sh (one command; file copies cannot self-wire) |
| git clone / brew checkout | scripts/install-policy-dispatch.sh (delegates to the same skill-embedded installer) |
The installer lints the registry before wiring, backs up settings, and is
idempotent. Disable per host with /plugin disable agentops or by removing the
two PreToolUse matchers from settings.
Contract tests: tests/scripts/policy-dispatch.bats (block+message+telemetry
per policy, stray-stdout hazard, waivers, audit/route modes, fail-open).
Minimal Python:
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
cmd = data.get('tool_input', {}).get('command', '')
if 'dangerous' in cmd:
print("Blocked: dangerous", file=sys.stderr)
sys.exit(2)
sys.exit(0) # Allow
Hook input (stdin):
{"tool_name":"Bash","tool_input":{"command":"npm test"},"session_id":"...","cwd":"..."}
| Variable | Scope | Purpose |
|----------|-------|---------|
| CLAUDE_PROJECT_DIR | All | Project root |
| CLAUDE_ENV_FILE | SessionStart/Setup | Persist env vars |
{"decision":"block","reason":"Tests failing. Fix before stopping."}
Critical: Check stop_hook_active to prevent infinite loops.
| Don't | Do |
|-------|-----|
| Old object format | Array format with matcher |
| Unquoted $VAR | "$VAR" |
| Exit 2 with JSON | Exit 2 uses stderr only |
| Skip stop_hook_active check | Always check in Stop hooks |
claude --debug # Hook execution details
/hooks # View/edit in REPL
~/.claude/settings.json or project .claude/settings.json, plus explicitly named hook scripts. The PreToolUse policy dispatcher ships by default (every install path wires it — see "Policy Dispatch Engine"); the additional guard recipes (skill-first coordination, standalone installed-skill-edit) stay inert until opted in.settings.json; give scripts descriptive executable filenames rather than embedding large shell programs in JSON.jq -e '.hooks | type=="object"' <settings.json> and a representative silent/fire test for each matcher; any parse error, noisy happy path, or recursion risk blocks activation.Take boshu2/agentops-cc-hooks from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.