Best Practices
How to pick hooks over other mechanisms, write ones you can trust, test them without running Command Code, and debug the ones that misfire.
Hooks are the right tool when you need deterministic, out-of-model enforcement in your workflows. When to use hooks vs. other Command Code features:
| Mechanism | When to use |
|---|---|
| Hooks | Block destructive actions, audit every tool call, inject context the model must see, halt the session on a signal |
| Skills | Give the model a workflow it can choose to invoke |
| Slash commands | Let the user trigger a fixed prompt or action |
AGENTS.md | Describe project norms the model follows by default but can deviate from |
- Parse stdin with
jq -r, nevereval. Everything intool_inputcame from the model and should be treated as untrusted. - Quote every variable before passing it to shell. The
deny-dangerous.shexample usesgrep -qEon a quotedprintf, nevereval $cmd. - Keep
timeouttight (10 seconds or less for sync hooks). A slow hook blocks the tool call and makes Command Code feel laggy. - Prefer
additionalContextoversystemMessagewhen guiding the model, andsystemMessagewhen explaining a policy violation to the user.
You don't need to run Command Code to iterate on a hook. Pipe a fake payload in:
Inspect stdout (JSON, or empty for "no opinion") and the exit code (echo $?). Ensure the script has execute permissions (chmod +x) to run as a hook in Command Code.
What you'll see when a hook doesn't behave the way you expect:
| Symptom | Likely cause | Fix |
|---|---|---|
| Hook never runs | You're in plan mode (hooks are skipped). The matcher regex doesn't match any of SHELL, READ, WRITE, EDIT. The script isn't executable | Exit plan mode. Check matcher against the display names SHELL, READ, WRITE, EDIT. Run chmod +x for your executables. |
Tool runs despite "deny" | permissionDecision is misspelled, or hookSpecificOutput is missing | Validate output against the reference schema |
| Timeout errors in the log | Hook takes longer than its timeout | Raise timeout, or move slow work to a background process |
| Hook crashes silently | Stdout is invalid JSON on exit 0 | Return empty stdout for "no opinion", or emit valid JSON |
jq: command not found | jq isn't installed on the machine running Command Code | brew install jq, or rewrite the hook in Python or Node |
For all of these, cmd --debug logs each hook evaluation (see below).
When a hook fires but doesn't behave the way you expect, run Command Code with --debug and tail the log:
The log records every hook evaluation: trust checks, config loads, matcher decisions, stdin/stdout payloads, and non-zero exit codes. You can see exactly why a hook was (or wasn't) invoked and what it returned.
The log file only exists while --debug is active and appends across sessions, so clear it between runs if noisy.
Hooks fire on every matching tool call. A slow hook means added latency on every matching tool call.
- Keep hooks fast. Well under a second. Users notice lag immediately.
- Push slow work to
PostToolUse. It runs after the tool completes and in parallel, so slowness there doesn't block the agent. - Move truly slow work out of process. If you need to talk to a SIEM or policy server, send a fire-and-forget HTTP request or append to a local log. Don't block on the round trip.
- Hooks overview: back to the big picture
- Examples: reference working hooks
- Hooks Reference: full schema for input, output, and exit codes
- Join our Discord community for support