Hooks

Hooks are shell commands that run at IsonForge lifecycle events. Use them to enforce policy, auto-format code, run linters, or block dangerous tool calls.

Configuration

Hooks live in ~/.isonforge/settings.json (user) or <cwd>/.isonforge/settings.json (project):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {"type": "command", "command": "./scripts/check-license.sh", "timeout": 10}
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{"type": "command", "command": "pnpm format"}]
      }
    ],
    "UserPromptSubmit": [
      {"hooks": [{"type": "command", "command": "./scripts/audit-log.sh"}]}
    ]
  }
}

Lifecycle events

Event Fires Can block?
SessionStart Once when the session starts No
UserPromptSubmit Before each user prompt goes to the agent Yes (exit 2)
PreToolUse Before each tool call Yes (exit 2 or permissionDecision: deny)
PostToolUse After each tool call No
Stop When the session exits No

Matcher

matcher is a pipe-delimited regex matched against the tool name (for PreToolUse / PostToolUse). Common values:

Hook command

Each hook entry is {"type": "command", "command": "...", "timeout": 30}:

The JSON context looks like:

{
  "event": "PreToolUse",
  "session_id": "abc12345",
  "cwd": "/home/user/proj",
  "tool_name": "edit_file",
  "tool_input": {"path": "src/auth.py", "old_string": "...", "new_string": "..."}
}

Use jq to read it:

#!/usr/bin/env bash
PATH_ARG=$(jq -r '.tool_input.path' < /dev/stdin)
if [[ "$PATH_ARG" == "src/secrets.py" ]]; then
  echo "Editing secrets.py blocked by policy" >&2
  exit 2
fi

Exit codes

Code Effect
0 Success. stdout (if any) is shown in the REPL.
2 BLOCK. For PreToolUse, the tool call is cancelled. For UserPromptSubmit, the prompt is cancelled.
Other non-zero Warning printed; execution continues.

JSON output (PreToolUse only)

For more nuanced control, output JSON to stdout:

{
  "hookSpecificOutput": {
    "permissionDecision": "deny"
  }
}

Or "allow" to skip the permission prompt entirely, or "ask" to force a prompt regardless of mode. This is independent of the exit-2 block - JSON is parsed before exit-code logic.

Environment

When a hook runs:

Example: auto-format after edits

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {"type": "command", "command": "pnpm exec prettier --write $(jq -r .tool_input.path)"}
        ]
      }
    ]
  }
}

Example: block edits outside the repo

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {"type": "command", "command": "./scripts/check-path.sh"}
        ]
      }
    ]
  }
}
#!/usr/bin/env bash
# scripts/check-path.sh
P=$(jq -r '.tool_input.path' < /dev/stdin)
ROOT=$(git rev-parse --show-toplevel)
ABS=$(realpath "$P")
if [[ "$ABS" != "$ROOT"/* ]]; then
  echo "Refusing to edit $P (outside repo)" >&2
  exit 2
fi

Example: gate prompts via secret scanner

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {"type": "command", "command": "./scripts/secret-scan.sh"}
        ]
      }
    ]
  }
}

Detects accidental API keys / passwords in the user prompt and blocks submission.

Example: audit log on session end

{
  "hooks": {
    "Stop": [
      {"hooks": [{"type": "command", "command": "./scripts/audit-log.sh"}]}
    ]
  }
}

View configured hooks

/hooks

Lists every hook from settings.json with event, matcher, command preview.

Best practices