How to Configure hooks.json to Add Custom PreToolUse Guards in Claude Code Harness

Edit hooks/hooks.json to define matcher patterns and hook chains for the tools you want to guard, synchronize the changes to .claude-plugin/hooks.json, and implement hooks that return exit code 0 to allow execution or JSON with "permissionDecision":"deny" to block it.

Claude Code Harness provides a PreToolUse hook system that intercepts tool calls (like Write, Edit, or Bash) before they execute, allowing you to inject custom guardrails for security and policy enforcement. By configuring hooks.json, you create automated checks that run in go/internal/guardrail/pre_tool.go, evaluating each tool invocation against your defined rules and determining whether to allow, deny, or defer the operation.

Understanding the PreToolUse Hook Architecture

Dual-File Configuration System

The harness maintains hook definitions in two locations that must remain synchronized according to .claude/rules/hooks-editing.md:

When a session starts, the harness loads .claude-plugin/hooks.json and builds an event-to-matcher mapping. The PreToolUse event slice determines which guards execute before specific tools run:

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit|MultiEdit|Bash|Read",
      "hooks": [{ "type": "command", "command": "...", "timeout": 10 }]
    },
    {
      "matcher": "AskUserQuestion",
      "hooks": [{ "type": "command", "command": "...", "timeout": 5 }]
    }
  ]
}

Runtime Execution in pre_tool.go

The guardrail implementation in go/internal/guardrail/pre_tool.go handles the actual enforcement:

  1. Collects all hook entries matching the tool's name against the matcher pattern (e.g., regex or simple glob).
  2. Executes hooks sequentially, piping a JSON payload containing tool arguments and session metadata to each process.
  3. Merges hookSpecificOutput objects from multiple hooks, applying precedence rules where a deny in settings.json overrides any hook-level allow (as of v2.1.77).

Hooks must return specific exit codes:

  • Exit 0: Allow the tool execution.
  • Exit 2: Block the tool (or return JSON with "permissionDecision":"deny").
  • Exit 1: Defer execution in headless mode (requires claude -p --resume to continue, available since v2.1.89).

Step-by-Step: Adding a Custom PreToolUse Guard

To configure hooks.json with custom guardrails, follow this workflow:

  1. Edit the source file at hooks/hooks.json.
  2. Define a matcher targeting specific tools (e.g., Bash, Write) using pipe-separated lists or glob patterns.
  3. Select a hook type:
    • command: Shell scripts for simple checks (fastest, simplest).
    • agent: Lightweight LLM review for contextual decisions (ideal for security scanning).
    • http: External policy service integration.
    • prompt: Interactive user confirmation dialogs.
  4. Sync the plugin cache by running ./scripts/sync-plugin-cache.sh after editing both JSON files.
  5. Validate your configuration with ./tests/validate-plugin.sh to ensure JSON validity and cache consistency.

PreToolUse Hook Configuration Examples

Command Hook: Block Dangerous Git Operations

Use a command hook to prevent destructive commands like git push --force or git reset --hard:

{
  "matcher": "Bash(git push --force*)",
  "hooks": [
    {
      "type": "command",
      "command": "/bin/bash -c 'cat <<EOF\n{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Force-pushing is forbidden by policy.\"}}\nEOF'",
      "timeout": 5
    }
  ]
}

This matcher captures any Bash tool call starting with git push --force. The command outputs a JSON payload that triggers a denial with a clear policy reason displayed to the user.

Agent Hook: Scan for Hard-Coded Secrets

Deploy an agent-based guard to inspect file contents before Write or Edit operations:

{
  "matcher": "Write|Edit",
  "hooks": [
    {
      "type": "agent",
      "prompt": "You are a security reviewer. Inspect the $ARGUMENTS JSON (it contains the file content about to be written). If you see any secret-like strings (e.g., AWS keys, passwords, private keys) return:\n{\"ok\": false, \"reason\": \"Potential secret detected\"}\nOtherwise return {\"ok\": true}.",
      "model": "haiku",
      "timeout": 30
    }
  ]
}

The agent receives tool input via the $ARGUMENTS environment variable. It returns {"ok": true} to allow or {"ok": false, "reason": "..."} to deny, which the harness translates into the appropriate permission decision automatically.

HTTP Hook: External Policy Service Integration

Forward PreToolUse data to a corporate policy engine for centralized governance:

{
  "matcher": "Write|Edit|Bash",
  "hooks": [
    {
      "type": "http",
      "url": "https://policy.example.com/pretool",
      "timeout": 10,
      "headers": {
        "Authorization": "Bearer $POLICY_TOKEN"
      },
      "allowedEnvVars": ["POLICY_TOKEN"]
    }
  ]
}

The harness POSTs the PreToolUse JSON payload to the specified URL. A 2xx response with an empty body allows execution, while a 2xx response containing the standard hook JSON schema can deny or modify the decision. Never embed secrets directly in the JSON; always use allowedEnvVars to permit environment variable substitution.

Critical Configuration Considerations

Synchronization Requirements: Both hooks/hooks.json and .claude-plugin/hooks.json must contain identical entries. The harness reads only the .claude-plugin version at runtime, so forgetting to sync after edits results in stale guardrails.

Timeout Liability: Keep guardrails lightweight. Timeouts between 5–30 seconds are typical. Excessive delays increase latency for every tool invocation, degrading the interactive experience.

Precedence Rules: Starting with v2.1.77, a deny entry in settings.json always wins over a hook returning "permissionDecision":"allow". Design your guards knowing that global settings can override local hook decisions.

Security Best Practices: Use ${VAR} syntax for environment expansion in command strings, and declare sensitive variables in allowedEnvVars for HTTP hooks. Never commit API keys or tokens directly into hooks.json.

Deferral Mechanics: In headless mode (v2.1.89+), hooks can return {"permissionDecision": "defer"} or exit code 1 to pause the session, allowing human review before resuming with claude -p --resume.

Summary

Frequently Asked Questions

What file should I edit to add custom PreToolUse guards?

Edit hooks/hooks.json as the primary source file, then copy your changes to .claude-plugin/hooks.json or run ./scripts/sync-plugin-cache.sh to update the runtime configuration. The harness only reads the .claude-plugin version during execution.

What exit codes should my PreToolUse hook return?

Return exit code 0 to allow the tool to run, exit code 2 to block it (or print JSON with "permissionDecision":"deny"), and exit code 1 to defer execution in headless mode (requiring manual resume). These codes are processed by the logic in go/internal/guardrail/pre_tool.go.

How do I block specific Bash commands like git push --force?

Use a matcher pattern like "Bash(git push --force*)" in your hooks.json entry, combined with a command hook that outputs {"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"Your reason here"}} to stdout before exiting.

What is the difference between command and agent hooks?

A command hook executes a shell script or binary directly, making it fast and deterministic for simple pattern matching. An agent hook spins up a lightweight LLM (like haiku) to perform contextual analysis on variables like $ARGUMENTS, making it suitable for complex security scans that require semantic understanding of code or natural language.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →