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:

MechanismWhen to use
HooksBlock destructive actions, audit every tool call, inject context the model must see, halt the session on a signal
SkillsGive the model a workflow it can choose to invoke
Slash commandsLet the user trigger a fixed prompt or action
AGENTS.mdDescribe project norms the model follows by default but can deviate from

  • Parse stdin with jq -r, never eval. Everything in tool_input came from the model and should be treated as untrusted.
  • Quote every variable before passing it to shell. The deny-dangerous.sh example uses grep -qE on a quoted printf, never eval $cmd.
  • Keep timeout tight (10 seconds or less for sync hooks). A slow hook blocks the tool call and makes Command Code feel laggy.
  • Prefer additionalContext over systemMessage when guiding the model, and systemMessage when 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:

cat <<'EOF' | COMMANDCODE_PROJECT_DIR="$PWD" COMMANDCODE_SESSION_ID=test COMMANDCODE_HOOK_EVENT=PreToolUse ./.commandcode/hooks/deny-dangerous.sh { "session_id": "test", "transcript_path": "/tmp/t.jsonl", "cwd": ".", "hook_event_name": "PreToolUse", "permission_mode": "standard", "tool_name": "shell_command", "tool_display_name": "SHELL", "tool_input": { "command": "rm -rf /" } } EOF

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:

SymptomLikely causeFix
Hook never runsYou're in plan mode (hooks are skipped). The matcher regex doesn't match any of SHELL, READ, WRITE, EDIT. The script isn't executableExit 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 missingValidate output against the reference schema
Timeout errors in the logHook takes longer than its timeoutRaise timeout, or move slow work to a background process
Hook crashes silentlyStdout is invalid JSON on exit 0Return empty stdout for "no opinion", or emit valid JSON
jq: command not foundjq isn't installed on the machine running Command Codebrew 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:

cmd --debug # in another terminal tail -f ~/.commandcode/logs/command.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.