Skip to main content

Hooks

Hooks are shell commands triggered by lifecycle events. Caveman Code matches Claude Code's settings.json schema verbatim — paste your existing ~/.claude/settings.json into ~/.cave/settings.json and your hooks Just Work.

Events

EventWhen firesSync?
SessionStartCaveman Code session bootssync, advisory
SessionEndCaveman Code exitssync, advisory
UserPromptSubmitUser sends a turnsync, advisory (stdout → context)
StopModel returns final responsesync, advisory
SubagentStopA subagent returns to parentsync, advisory
PreToolUseBefore any tool callsync, blocking, 30s timeout
PostToolUseAfter any tool callasync by default
PreCompactBefore context compactionsync, advisory
PostCompactAfter context compactionsync, advisory
NotificationStatus / progress eventsasync, fire-and-forget
FileChangedWatched file editsasync
CwdChangedcd inside the sessionsync, advisory

settings.json schema

{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool": "Edit|Write", "paths": ["src/**/*.ts"] },
"command": ["bash", "-lc", "biome check --staged"],
"timeout": 30,
"decision": "deny-on-nonzero"
}
],
"PostToolUse": [
{
"matcher": { "tool": "Edit" },
"command": ["bash", "-lc", "biome format --write \"$CAVE_HOOK_FILES\""]
}
],
"Stop": [
{
"command": ["bash", "-lc", "npm test --silent"],
"decision": "advisory"
}
]
}
}

Matchers

Matcher keyPurpose
toolRegex against tool name. Edit|Write matches both.
pathsGlob patterns. Hook only fires if a tool argument is a path under a glob.
providerRestrict by active provider.
cwdGlob against the session's cwd.
argumentsJSONPath-ish match against tool arguments.

Decisions

PreToolUse hooks return one of:

DecisionEffect
allowTool call proceeds. Default if exit 0.
denyTool call denied. Reason fed back to the model. Exit 2 from the hook.
askUser is prompted before the tool call.
deferSkip this hook this turn (used by recipes).

PostToolUse and other events: stdout from the hook is appended to the model's context as a system reminder. Exit code is logged but not used to gate.

stdout-as-assistant-context (the killer feature)

Anything a hook prints to stdout is fed back to the model as a system reminder. Use this to:

  • Inject the latest CI status before the model decides how to fix.
  • Re-fetch the user's recent commits so the model knows the diff is fresh.
  • Run a linter and let the output guide the model's next edit.

Example: a PostToolUse hook that reports failing tests:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool": "Edit|Write" },
"command": ["bash", "-lc", "npm test --silent --json | jq '.numFailedTests' || true"]
}
]
}
}

If the count is non-zero, the model sees 123 in its context and proactively fixes failures.

Default hooks shipped with Caveman Code

HookEventPurpose
auto-formatPostToolUse Edit/WriteRun Biome / prettier on changed files
auto-testStopRun the test suite, report failures
commit-gatePreToolUse Bash matching git commitEnforce conventional-commit format
secret-scanPreToolUse WriteBlock writes that contain secrets (gitleaks / trufflehog)

Disable any of these in settings.json by setting enabled: false.

Slash commands

caveman hooks list # all hooks, scope, status
caveman hooks test PreToolUse --tool Edit --path src/foo.ts

/hooks opens the same view inside the TUI.

Importing Claude Code hooks

cp ~/.claude/settings.json ~/.cave/settings.json
# adjust permission mode if needed; the rest works as-is

Anti-patterns

  • Long blocking PreToolUse hooks — 30s timeout is hard. Move heavy work to PostToolUse.
  • Mutating files in PostToolUse without re-reading — the model's context still shows the pre-mutation file. Pair with a read directive in the next turn.
  • Hooks where skills would fit — hooks enforce invariants; skills express knowledge. Pick correctly.