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

> Learn to configure hooks.json in Claude Code Harness with custom PreToolUse guards. Block or allow tool execution by defining matcher patterns and hook chains.

- Repository: [Chachamaru/claude-code-harness](https://github.com/Chachamaru127/claude-code-harness)
- Tags: how-to-guide
- Published: 2026-05-28

---

**Edit [`hooks/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks.json), you create automated checks that run in [`go/internal/guardrail/pre_tool.go`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude/rules/hooks-editing.md):

- **[`hooks/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json)** — The source file developers edit directly.
- **[`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json)** — The runtime file the plugin reads during session initialization.

When a session starts, the harness loads [`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json) and builds an event-to-matcher mapping. The **PreToolUse** event slice determines which guards execute before specific tools run:

```json
{
  "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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./scripts/sync-plugin-cache.sh) after editing both JSON files.
5. **Validate** your configuration with [`./tests/validate-plugin.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./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`:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json) and [`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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

- **Edit [`hooks/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json)** as your source of truth, then sync to [`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json) using [`./scripts/sync-plugin-cache.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./scripts/sync-plugin-cache.sh).
- **Implement guards** using command, agent, http, or prompt hooks, choosing based on complexity and latency requirements.
- **Control execution** by returning exit code 0 (allow), exit code 2 (deny), or JSON with specific `permissionDecision` values.
- **Reference [`go/internal/guardrail/pre_tool.go`](https://github.com/Chachamaru127/claude-code-harness/blob/main/go/internal/guardrail/pre_tool.go)** for the core execution logic that processes your hook chains and merges outputs.
- **Validate changes** with [`./tests/validate-plugin.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./tests/validate-plugin.sh) to ensure runtime compatibility and JSON schema compliance.

## Frequently Asked Questions

### What file should I edit to add custom PreToolUse guards?

Edit **[`hooks/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json)** as the primary source file, then copy your changes to **[`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json)** or run [`./scripts/sync-plugin-cache.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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.