Examples

A set of working hooks you can copy into your project. Each shows a different use case, so you can modify them as you see fit. All are bash scripts here, but a hook can be any executable that parses JSON on stdin.

ExampleEventKindWhat you get
Block dangerous bashPreToolUseBlockingModel can't run rm -rf /, curl | sh
Warn on sensitive readsPreToolUseContext injectionModel is told to redact secrets before quoting
Audit tool callsPreToolUse or PostToolUseAudit onlyMatching tool calls appended to a local log file
Quality gateStopBlocking (retry)Assistant can't finish with DO NOT SHIP markers left in the code

Every example has two files:

  1. A settings.json that wires the hook.
  2. A shell script at .commandcode/hooks/<name>.sh.

Ensure each script is executable with chmod +x before Command Code fires the hook.

Each example ends with a Test block containing a prompt to paste into Command Code and the result to expect so you can confirm the hook fires.

Match the model's shell command against a short list of dangerous patterns. On a hit, deny the tool and tell the model why, so it doesn't retry the same pattern.

.commandcode/settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "shell", "hooks": [ { "type": "command", "command": "./.commandcode/hooks/deny-dangerous.sh", "timeout": 5 } ] } ] } }

.commandcode/hooks/deny-dangerous.sh

#!/usr/bin/env bash set -euo pipefail cmd=$(jq -r '.tool_input.command // ""') # Dangerous patterns: rm -rf /, curl | sh, fork bomb, sudo rm pattern='rm[[:space:]]+-[rR]f?[[:space:]]+/' pattern+='|curl.*\|.*(sh|bash)' pattern+='|:\(\)[[:space:]]*\{' pattern+='|sudo[[:space:]]+rm' if printf '%s' "$cmd" | grep -qE "$pattern"; then jq -n --arg cmd "$cmd" '{ systemMessage: "blocked dangerous command", hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ("Command matched a dangerous pattern. Policy forbids: " + ($cmd | .[0:120])) } }' else exit 0 fi

Test: Block dangerous bash commands

Prompt in Command Code:

Try to run this shell command: rm -rf /tmp/cmd-hook-demo

Expected result:

  • ❌ The command is blocked before it runs (it matches the rm -rf / pattern, even though the target is harmless).
  • The agent sees "Command matched a dangerous pattern. Policy forbids…" and moves on without retrying.

How it works

  • Reads tool_input.command from stdin with jq.
  • Matches against four patterns: rm -rf /, curl | sh, a :(){} fork bomb, sudo rm.
  • On a hit, emits permissionDecision: "deny". The tool is skipped and the model receives permissionDecisionReason as the tool result.
  • On a miss, exits 0 with no stdout. The tool runs normally.

Allow every read, but quietly inject a note when the path looks sensitive. The model sees the note as extra context and adjusts its response.

.commandcode/settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "read", "hooks": [ { "type": "command", "command": "./.commandcode/hooks/warn-sensitive-reads.sh" } ] } ] } }

.commandcode/hooks/warn-sensitive-reads.sh

#!/usr/bin/env bash set -euo pipefail path=$(jq -r '.tool_input.absolute_path // ""') # Default note. Upgraded to a strong warning when the path looks sensitive. ctx="File is being read under an audit hook. Do not paste full contents unless the user asks." if printf '%s' "$path" | grep -qE '(\.ssh/|/\.env$|\.pem$|id_rsa)'; then ctx="SENSITIVE READ: $path. Redact any keys or tokens before quoting from this file." fi jq -n --arg ctx "$ctx" '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", additionalContext: $ctx } }'

Test: Warn on sensitive reads

Prompt in Command Code:

Read the .env file in my home directory

Expected result:

  • ✅ The read is allowed. The hook never blocks it.
  • The agent receives the injected note "SENSITIVE READ: …/.env. Redact any keys or tokens before quoting from this file." and refuses to paste the full contents.

How it works

  • Always returns permissionDecision: "allow". The Read is never blocked.
  • Sends additionalContext to the model, appended to the tool result before the next turn.
  • Upgrades the note to an explicit redaction warning when the path matches .ssh/, .env, .pem, or id_rsa.

Log every matching tool call to an append-only file. The hook writes nothing to stdout, so it's observe-only: the tool runs unchanged. The pattern (read tool_input with jq, append a tab-separated line) is the same for any tool. Swap the event, matcher, and extracted field to fit what you want to observe.

Use PostToolUse to log what completed. Use PreToolUse to log what was attempted; this also catches commands that a later hook denies, as long as the audit hook runs first.

Writes and edits (PostToolUse)

Fires after the file mutation completes. Uses COMMANDCODE_PROJECT_DIR and COMMANDCODE_SESSION_ID directly, so there's no need to parse cwd or session_id from stdin.

.commandcode/settings.json

{ "hooks": { "PostToolUse": [ { "matcher": "write|edit", "hooks": [ { "type": "command", "command": "./.commandcode/hooks/audit-writes.sh" } ] } ] } }

.commandcode/hooks/audit-writes.sh

#!/usr/bin/env bash set -euo pipefail LOG_FILE="$COMMANDCODE_PROJECT_DIR/.commandcode/write-audit.log" # Extract the target file path from the JSON payload on stdin. path=$(jq -r '.tool_input.file_path // "?"') timestamp=$(date -u +%FT%TZ) # Append one tab-separated line per write: timestamp, session ID, file path. printf '%s\t%s\t%s\n' "$timestamp" "$COMMANDCODE_SESSION_ID" "$path" >> "$LOG_FILE"

Test: Audit writes

Prompt in Command Code:

Create a new file called config.txt with some test data

Expected result:

  • ✅ The file is created.
  • The hook appends one line to .commandcode/write-audit.log.
  • Verify with cat .commandcode/write-audit.log. You should see a tab-separated line (timestamp, session ID, file path).

Shell commands (PreToolUse)

Fires before execution, so this logs every shell command the agent issues.

.commandcode/settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "shell", "hooks": [ { "type": "command", "command": "./.commandcode/hooks/log-shell.sh" } ] } ] } }

.commandcode/hooks/log-shell.sh

#!/usr/bin/env bash set -euo pipefail LOG_FILE="/tmp/cmd-shell.log" # Extract the shell command from the JSON payload on stdin. command=$(jq -r '.tool_input.command // ""') timestamp=$(date -u +%FT%TZ) # Append one tab-separated line to the audit log. printf '%s\t%s\n' "$timestamp" "$command" >> "$LOG_FILE"

Test: Audit shell commands

Prompt in Command Code:

Run this command: ls -la /home Then tell me you're done.

Expected result:

  • ✅ The command runs as normal.
  • The hook appends one line to /tmp/cmd-shell.log.
  • Verify with cat /tmp/cmd-shell.log. Each entry in /tmp/cmd-shell.log has the shape (fields are tab-separated):
2026-04-21T17:38:51Z ls -la /home

Block the agent from finishing the turn while a DO NOT SHIP marker is still in the code. The Stop event fires at end of turn; this hook greps for the marker and exits 2 when it finds one, sending the assistant back for another pass with the offending lines as feedback. The stop_hook_active check is the canonical loop-prevention pattern. Without it, the hook would fire forever.

.commandcode/settings.json

{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "./.commandcode/hooks/no-ship-gate.sh" } ] } ] } }

.commandcode/hooks/no-ship-gate.sh

#!/usr/bin/env bash set -euo pipefail INPUT=$(cat) # Already retrying, so let the turn end and don't loop. if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0 fi # Quality gate: don't finish while "DO NOT SHIP" markers remain. # On a hit, echo the lines to stderr and exit 2. The engine feeds # that text to the model on the retry so it knows what to remove. # `.commandcode` is excluded so the hook can't match its own source. HITS=$(grep -rn "DO NOT SHIP" . \ --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.commandcode \ 2>/dev/null || true) if [ -n "$HITS" ]; then echo "Remove these 'DO NOT SHIP' markers before finishing:" >&2 echo "$HITS" | head -20 >&2 exit 2 fi

Test: Quality gate

Prompt in Command Code:

Write a JavaScript function that processes user data. Include this comment: // DO NOT SHIP - add error handling Then tell me you're done.

Expected result: the turn is blocked, the agent retries on its own, and it finishes once the marker is gone. Step by step:

  • First turn: the assistant finishes → the hook greps → finds the DO NOT SHIP marker → exits 2 with the matching lines on stderr. A gray Stop frame appears in the feed:
Ran 1 stop hook └ Stop hook [./.commandcode/hooks/no-ship-gate.sh] exited 2: Remove these 'DO NOT SHIP' markers before finishing: [retry 1/3]
  • The engine feeds that stderr text back to the model as a user-role message and re-runs the turn.
  • Second turn: the assistant removes the marker and adds error handling. The hook fires again with stop_hook_active: true → exits 0. The turn ends.

If the hook keeps blocking (e.g. it's broken), the engine caps retries at 3 and ends the turn with Stop hook retry cap reached (3). Ending turn. Same outcome ×4. Fix or disable: ./.commandcode/hooks/no-ship-gate.sh.