Shared hooks governance — repo + bootstrap + signing
Question 18 (Org enablement): How do you govern company-wide hooks (Stop, PreToolUse, PostToolUse)?
Max-score answer: Repo + auto-install via dotfiles bootstrap + signed/audited hook scripts.
Why it matters: Hooks run code on every tool call. Unaudited hook drift = supply-chain risk. But shared hooks done well are a 10× productivity multiplier.
Why this matters in 2026 (supply chain risk vs 10× multiplier)
Section titled “Why this matters in 2026 (supply chain risk vs 10× multiplier)”Hooks are the most powerful — and most dangerous — extension surface in Claude Code, Codex, and Cursor. A PreToolUse hook runs before every tool call the agent makes: every bash invocation, every file write, every MCP request. A PostToolUse hook runs after. A Stop hook runs when the agent says it’s done. Hooks see everything the agent does, can mutate or veto it, and execute as a normal process on the developer’s machine with the developer’s credentials, SSH keys, and cloud tokens.
That power cuts two ways. On the upside, hooks turn a probabilistic agent into a governed system. A PreToolUse hook grepping for rm -rf / blocks it deterministically — regex doesn’t get jailbroken. A PostToolUse running prettier --write after every edit gives the team formatting discipline for free. A Stop hook that auto-opens a PR collapses the “write code → ship code” loop from minutes to seconds. Teams using hooks well in 2026 report 5–10× faster cycle time on routine work.
On the downside, hooks are a textbook supply-chain target. The April 2026 wave of npm and GitHub-template attacks — Shai-Hulud, TeamPCP, the SAP-CAP compromise — included payloads that injected .claude/settings.json files with malicious SessionStart hooks into open-source repos, so that simply opening the repo in Claude Code or VS Code triggered credential exfiltration from the developer’s dotfiles. A single unaudited hook in a shared repo is, worst case, root on every developer machine.
The Q18 max-score answer closes both gaps at once. A single git repo owns every shared hook. A dotfiles bootstrap installs them idempotently. Scripts are signed so a tampered hook can’t run unnoticed. Every execution is logged so audit and incident response work. You get the 10× upside without the supply-chain risk.
What “max score” actually looks like (repo + auto-install + signed + audited)
Section titled “What “max score” actually looks like (repo + auto-install + signed + audited)”Four properties, each non-negotiable:
1. A company hooks repo. A single git repo (acme/agent-hooks) holds every shared hook. One folder per hook, with a hook.json declaring lifecycle event, tool matcher, owner, timeout. Reviewed via PR, has CODEOWNERS, ships through the same release process as production code.
2. Auto-install via dotfiles bootstrap. The shared dotfiles repo clones the hooks repo and writes entries into ~/.claude/settings.json, ~/.cursor/settings.json, and ~/.codex/config.toml. Idempotent, runs on day-1 onboarding, re-runs on every shell login so config self-heals. Devs never edit ~/.claude/hooks/ directly — they edit in the repo and submit a PR.
3. Signed hook scripts. Two patterns work: (a) signed git commits on branch-protected main, with the bootstrap refusing to install if git verify-commit fails; (b) sigstore / cosign attached signatures, with a pre-execution wrapper that calls cosign verify-blob. (b) is heavier but ships outside git without losing the chain.
4. Auditable execution. Every hook writes a structured JSON log line — timestamp, hook, event, tool, command, exit code, decision — to ~/.acme-hooks/audit.log and (optionally) to your SIEM. Read by an internal dashboard, reviewed quarterly. When something breaks, the log is the source of truth.
A new hire’s machine after the bootstrap:
$ ~/.acme-dotfiles/bootstrap.sh[ok] Cloned acme/agent-hooks to ~/.acme-agent-hooks[ok] Verified signed commits on origin/main[ok] Installed 12 hooks into ~/.claude/settings.json$ claude> /hooksPreToolUse: deny-dangerous-commands, redact-secrets, cost-capPostToolUse: auto-format, tag-ai-pr, run-typecheckStop: open-pr, sync-skills, notify-slackSessionStart: skills-sync, hooks-sync, audit-rotateNo “ask Slack which hooks to install.” No drift. No silent malicious hook landing in ~/.claude/hooks/.
Current landscape (web-search-verified)
Section titled “Current landscape (web-search-verified)”Company hook repo structure
Section titled “Company hook repo structure”The 2026 convention, set by Endor Labs’ Agent Governance writeup and reinforced by AxonFlow and HackerNoon’s Governance Layer for Claude Code, is one folder per hook, one metadata file per folder, language-agnostic. The hook returns allow/deny/modify via exit code or stdout JSON.
acme/agent-hooks/├── bootstrap.sh├── version.json├── .github/CODEOWNERS # security team owns /pre-tool-use/**└── hooks/ ├── deny-dangerous-commands/{hook.json, run.sh, policy.yaml, tests/} ├── auto-format/{hook.json, run.sh} ├── cost-cap/{hook.json, run.py} └── tag-ai-pr/{hook.json, run.sh}A minimal hook.json:
{ "name": "deny-dangerous-commands", "event": "PreToolUse", "matcher": "Bash", "command": "./run.sh", "timeout_ms": 1500, "owner": "security@acme.dev"}For PreToolUse hooks, the security team is on CODEOWNERS because those are the hooks with power to silently weaken every other control.
Bootstrap auto-install (dotfiles bootstrap.sh)
Section titled “Bootstrap auto-install (dotfiles bootstrap.sh)”The pattern that’s stuck across the dotfiles.github.io reference and modern org dotfiles:
#!/usr/bin/env bash# ~/.acme-dotfiles/bootstrap.sh — day-1 and shell loginset -euo pipefail
HOOKS_DIR="${HOME}/.acme-agent-hooks"SETTINGS="${HOME}/.claude/settings.json"
if [ ! -d "${HOOKS_DIR}" ]; then git clone --depth 50 https://github.com/acme/agent-hooks "${HOOKS_DIR}"else git -C "${HOOKS_DIR}" fetch --quiet origin main git -C "${HOOKS_DIR}" merge --ff-only origin/mainfi
# Verify signed commits (defense in depth)if ! git -C "${HOOKS_DIR}" verify-commit HEAD >/dev/null 2>&1; then echo "FATAL: ${HOOKS_DIR} HEAD is not signed. Refusing to install." exit 1fi
# Render settings.json from hook metadata (idempotent)"${HOOKS_DIR}/bin/render-settings.py" "${HOOKS_DIR}/hooks/" > "${SETTINGS}.tmp"mv "${SETTINGS}.tmp" "${SETTINGS}"
mkdir -p "${HOME}/.acme-hooks" && touch "${HOME}/.acme-hooks/audit.log"Three properties matter: idempotent (drift impossible — manual edits get overwritten on next login), verification-first (fail-closed default; a tampered repo refuses to install), and read-only on the developer’s side (edits go through PR, the bootstrap is the single source of truth).
Signing (PGP / sigstore / git-signed commits)
Section titled “Signing (PGP / sigstore / git-signed commits)”Three approaches work in 2026:
(a) Signed git commits. Lightest setup. Branch-protect main, require signed commits, CODEOWNERS routes PreToolUse PRs to security. Bootstrap runs git verify-commit HEAD and refuses to install on failure. Pros: zero new infra. Cons: verifies the commit, not the executable — a stolen signing key bypasses until rotation.
(b) Sigstore / cosign attached signatures. Heavier, stronger. CI signs each hook (or the hooks/ tarball) with cosign using your org’s keyless or OIDC identity. Bootstrap calls cosign verify-blob --certificate-identity=ci@acme.dev. Binds the signature to the CI pipeline’s identity, so a stolen developer key alone can’t forge a hook. Integrates with SLSA provenance.
(c) PGP-signed releases. Traditional Linux-distro approach. CI builds and PGP-signs hooks-2026.05.21.tar.gz. Bootstrap downloads and gpg --verifys. Well-understood, offline-verifiable; PGP key management is famously painful and most teams move to (b) after a few months.
Most teams start with (a) for week-1 and add (b) within a quarter.
Audit (logging every hook execution, periodic review)
Section titled “Audit (logging every hook execution, periodic review)”Every hook script wraps its body in a logging shim that writes a JSON line to ~/.acme-hooks/audit.log:
#!/usr/bin/env bashinput="$(cat)"cmd="$(echo "$input" | jq -r '.tool_input.command // empty')"
decision="allow"; reason=""if [[ "$cmd" =~ rm[[:space:]]+-rf[[:space:]]+/ ]]; then decision="deny"; reason="destructive recursive delete on root"fi
jq -n --arg ts "$(date -u +%FT%TZ)" --arg hook "deny-dangerous-commands" \ --arg cmd "$cmd" --arg decision "$decision" --arg reason "$reason" \ '{ts:$ts, hook:$hook, cmd:$cmd, decision:$decision, reason:$reason}' \ >> "${HOME}/.acme-hooks/audit.log"
[ "$decision" = "deny" ] && { echo "{\"continue\": false, \"reason\": \"$reason\"}"; exit 0; }echo '{"continue": true}'The audit log lets you answer the questions that matter when something breaks: did any hook deny a tool call in the last hour? Did the new cost-cap trigger on Mary’s machine before she opened the bug? Did any hook exit non-zero since the last release? Without the log, all of those become guesswork.
Periodic review is the other half. At least quarterly, a security or platform engineer skims aggregated audit data (denials per hook, error rates, hooks that never fired). Zero-hit hooks are deletion candidates. High error rates need a bug filed against the owner. Surprising denial patterns are an incident signal.
Useful shared hooks (auto-format, AI-PR label, dangerous-command deny, cost cap)
Section titled “Useful shared hooks (auto-format, AI-PR label, dangerous-command deny, cost cap)”A starter set that pays back the governance overhead in one sprint:
PreToolUse·deny-dangerous-commands. Regex-blockrm -rf /,dd of=/dev/, fork bombs,curl ... | bash. Deterministic, jailbreak-proof.PreToolUse·redact-secrets. Scan tool inputs for AWS/GitHub/OpenAI/Anthropic/Stripe keys. Deny with “paste from your password manager.”PreToolUse·cost-cap. Above the per-dev cap, deny expensive-looking calls. Strongest cost control there is, provider-agnostic.PostToolUse·auto-format. After everyEdit/Write, runprettier --write/gofmt/ruff format. Removes formatting review noise entirely.PostToolUse·run-typecheck. On every TS write,tsc --noEmit --incrementaland feed errors back. Agent fixes type errors before review.Stop·tag-ai-pr. Label PRai-authored(Q11). Drives the extra-gates pipeline.Stop·open-pr. Push andgh pr createwhen the agent says it’s done.SessionStart·skills-sync/hooks-sync. Auto-update the skills and hooks repos so devs are never on stale config.
Each hook does one thing, ships through the repo, has an owner, runs under audit. A ten-line bash auto-formatter is fine. A ten-line auto-formatter that also silently exfiltrates env vars is the problem governance prevents.
Step-by-step: rolling out governed hooks
Section titled “Step-by-step: rolling out governed hooks”-
Seed the repo with two safe hooks. Pick two with obvious upside and small blast radius:
PostToolUse · auto-formatandStop · tag-ai-pr. Neither breaks dev work on misfire. Open a PR, merge. -
Wire the dotfiles bootstrap on your own machine. Add the script above to your dotfiles repo. Run it. Confirm
~/.claude/settings.jsonwas rewritten and both hooks list under/hooks. Break it (edit settings.json by hand), re-run, confirm self-heal. -
Turn on signed commits, branch protection, CODEOWNERS. Branch-protect
main, require signed commits and PR review. Routehooks/pre-tool-use/**to security,post-tool-use/**andstop/**to platform. Addgit verify-commit HEADto the bootstrap. An unsigned commit onmainis now a deploy-blocking incident. -
Add the audit log and a tiny dashboard. Wrap every hook with the JSON-line logging shim. Pipe
audit.logto your central logging. Build a one-page dashboard: executions per day, denials per day per hook, error rate, hooks with zero hits in 30 days. Without it you won’t look; with it the data drives decisions. -
Roll out to one team first, not the org. Pick five to ten engineers. Sit with them through the bootstrap. Watch what breaks (corporate VPN proxies, non-standard
$HOME, Windows + WSL). Watch the audit log for false-positive denials — every one is a hook bug, not a developer problem. Then expand. -
Add the high-value
PreToolUsehooks once trust is built. After two clean weeks ofauto-format, adddeny-dangerous-commandsandredact-secrets. Ship behind a feature flag (disabled: truefield a developer can flip for an hour). Watch the log two weeks. Once they deny real bad inputs and never legit work, remove the disable hatch. -
Add cost caps and SessionStart sync. Once basics are stable, add
cost-cap(Q4) andSessionStart · hooks-sync/skills-sync(Q6). First gives finance the off-switch; second makes the next update reach every machine without a Slack message. -
Rotate keys, review quarterly. Calendar event: every quarter, platform rotates signing keys, audits CODEOWNERS for stale members, runs
git log --since=...against the hooks repo to confirm every change went through PR review. An unsigned merge or a deleted audit line is an incident, not a finding.
Common pitfalls
Section titled “Common pitfalls”Unsigned hooks. A team copies a hook from a blog post directly into ~/.claude/hooks/. Six months later, someone forks the dotfiles, swaps in a malicious version, and the next bootstrap.sh installs it everywhere. Fix: make unsigned hooks unrunnable. If your bootstrap installs whatever it finds, you don’t have hook governance.
No audit. “We have hooks but we don’t log anything.” Hooks work most of the time, but when one starts blocking legit work, you find out via Slack DM, not the dashboard you didn’t build. The audit log is one jq line per hook.
Hook drift. Every developer hand-editing settings.json ends with no two machines running the same config. The bootstrap-on-every-login pattern fixes it — but only if local edits don’t stick. Carve out ~/.claude/hooks.local/ for personal hooks and leave it alone.
Blocking devs. A buggy PreToolUse denying legit calls is worse than no hook. Devs will route around it — uninstalling, disabling, or switching tools. Ship PreToolUse behind a feature flag for two weeks. Include a documented escape hatch. Treat every false denial as a P1 bug.
Hooks that need network. A hook calling a slow API on every tool call makes the agent unusable. Hooks are sub-100ms or fire-and-forget. The 1.5s timeout_ms exists for this reason; track p95 duration in the audit log.
Forgetting Windows / WSL. Bootstrap scripts on macOS often assume bash and /usr/local. Test every supported platform before declaring rollout done.
Owner-less hooks. Decay the moment their author leaves. Archived on the next quarterly review — no exceptions.
Mixing personal and company hooks. Namespace company hooks under ~/.claude/hooks/.acme/ and leave the rest alone. A git pull should never clobber personal experiments.
How to verify you’re there
Section titled “How to verify you’re there”- A company
agent-hooksrepo exists with at least 5 production-quality hooks. - Each hook folder has a
hook.jsondeclaring event, matcher, owner, timeout. - Repo has branch protection on
main, requires signed commits, hasCODEOWNERSroutingPreToolUseto security. -
bootstrap.shlives in shareddotfiles. Clones, verifies signed commits, deterministically rewrites~/.claude/settings.json. - Bootstrap is idempotent — re-running it on a machine with manual
settings.jsonedits restores canonical state. - Bootstrap is part of new-hire day-1 setup. New engineers walk in with all hooks installed and verified.
- Every hook execution writes a JSON line to
~/.acme-hooks/audit.log, streamed to central logging. - An internal dashboard shows executions per day, denials per day, p95 duration, zero-hit hooks.
- A documented quarterly review rotates signing keys, audits CODEOWNERS, archives stale hooks, reviews the dashboard.
- At least one
PreToolUse · deny-dangerous-commandsis in production with at least one logged real denial in the last quarter. - Opening a fresh Claude Code session on a new machine and running
/hookslists the same hooks as everywhere else in the org.