Denied by PreToolUse hook from repo settings (hook errored)
This message means a PreToolUse hook from the project’s settings failed, and Claude Code blocked the tool call because of the failure. The hook did not choose to deny anything. It crashed, could not start, timed out, or printed output Claude Code could not read.
Fix the hook and the call goes through. Below: how to find it, why it failed, how to test it by hand, and how to decide whether a broken hook should block or let calls through.
What the message says
Read it in three parts.
- Denied by PreToolUse hook. The block came from a
PreToolUsehook, which runs before every matching tool call. It is not a permission rule, and it is not a prompt you declined. - From “repo settings”. The hook is defined in the project’s settings:
.claude/settings.json(committed with the repo) or.claude/settings.local.json(your local override). It is not in your user settings, a plugin or a skill. The hooks reference lists every place a hook can live. - Hook errored. The hook failed instead of returning a verdict.
That last part matters, because by default a failing hook does not block. The official reference says a hook that exits with a code other than 0 or 2, “or fails in some other way, such as not starting or printing invalid JSON”, is a non-blocking error and “the action goes ahead, except for hooks whose failure blocks the action”. On PreToolUse you normally see a <hook name> hook error notice in the transcript and the tool still runs.
So when the error turns into a deny, one of two things is true:
- The hook sets
"onFailure": "block". This field (Claude Code v2.1.295 or later) tells Claude Code to block the action when acommandorhttphook fails or times out. The notice readsfailed; blocking because onFailure is "block", ortimed outafter a timeout. - You are on Windows without Git Bash. There, a failed shell-form
PreToolUsehook withonFailureunset can block the call. The notice saysblocking because Git Bash wasn't found, so only exit code 0 or 2 counts as this hook's verdict.
If neither applies and the call was still blocked, check whether the hook exited 2 on purpose. Exit 2 is a deliberate block, shown as PreToolUse:Bash hook error: [<command>]: <stderr>. That is a policy verdict, not a bug.
Find the hook
Type /hooks inside Claude Code. It opens a read-only list of every configured hook, labeled by source: user settings, project settings, local settings, plugin, or session. Select the hook to see the exact command and the file it came from.
Then open that file. A typical project hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.sh",
"timeout": 30,
"onFailure": "block"
}
]
}
]
}
}
The matcher says which tools trigger it. Bash is an exact match. Edit|Write is a list. Anything with other characters is an unanchored regular expression, so Edit.* also matches NotebookEdit. If every tool call is failing, look for a matcher of "*", "", or none.
The notice shows only the first line of stderr. For the full stderr and the hook’s stdout, start a session with claude --debug (or type /debug in a fresh session) and read the debug log.
The usual causes
Each of these comes from the failure list in the hooks reference and the troubleshooting section of the hooks guide.
The script path is wrong. The notice carries the shell’s own message:
Failed with non-blocking status code: /bin/sh: /path/to/hook.sh: No such file or directory
With onFailure: "block" the same missing file becomes a deny. Relative paths break as soon as the session’s working directory moves. Use ${CLAUDE_PROJECT_DIR}, which points at the project root where the session started, and quote it in shell form. In a worktree it still points at the main checkout.
The script is not executable. The shell exits with a code like 127 and the hook never runs:
chmod +x .claude/hooks/check-command.sh
This bites after a fresh clone, or a checkout on a filesystem that drops the execute bit.
The interpreter is missing. jq: command not found, node: command not found, or a shebang that points at a binary your machine does not have. The author had it; you do not. Install it, or switch the hook to an interpreter you know is there. The guide suggests Python or Node.js when jq is missing.
The JSON on stdout is broken. If the hook exits 0 and prints JSON that does not parse or fails schema validation, that counts as a failure, and with onFailure: "block" it blocks. Build the payload with an encoder such as jq -n instead of string concatenation, so quotes and backslashes are escaped. Print nothing but the JSON object.
One related trap raises no error at all. An unconditional echo in your shell profile lands on stdout before the JSON, Claude Code reads the output as plain text, and your decision is silently ignored. Search the debug log for Hook output does not start with {.
The hook timed out. Command hooks default to 600 seconds. A hook that reaches its timeout is cancelled and its output discarded. Without onFailure: "block" the call continues through the normal permission flow. With it, the call is blocked. A hook that calls a network service needs a short timeout and a plan for when the service is down.
It exited 1. This catches people who write policy hooks. Exit 1 is the Unix failure code, but in Claude Code it is not a deny. The reference says: “Exit code 1 without valid JSON output is a non-blocking error unless the hook is one whose failure blocks the action… If your hook is meant to enforce a policy, use exit 2.” Under onFailure: "block", any exit other than 0 or 2 counts as a failure even if the hook printed permissionDecision: "allow".
Exit codes in one table
| Exit code | Stdout | What Claude Code does on PreToolUse |
|---|---|---|
| 0 | Nothing or plain text | No decision; the normal permission flow applies |
| 0 | Valid JSON | The JSON decides: allow, deny, ask, or defer |
| 0 | Invalid JSON | Failure (non-blocking unless the hook blocks on failure) |
| 2 | Anything | Blocks the call; stderr is the reason the model sees |
| Other | Anything else | Failure (non-blocking unless the hook blocks on failure) |
The full matrix is in the reference. Our Claude Code hooks reference covers the input and output contract in more depth.
Test the hook by hand
Feed it a tool call on stdin, the way Claude Code would, and check the exit code:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./.claude/hooks/check-command.sh
echo $?
That is the minimal shape from the hooks guide. A real PreToolUse input carries more fields. If your script reads them, test with the fuller shape:
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
},
"tool_use_id": "toolu_01ABC123"
}
Save it as sample.json and run:
CLAUDE_PROJECT_DIR="$PWD" ./.claude/hooks/check-command.sh < sample.json
echo "exit: $?"
Set CLAUDE_PROJECT_DIR if the script uses it. Claude Code exports it to the hook process; your terminal does not. Check three things: the exit code is 0 or 2, stdout is empty or one JSON object, and the run finishes well inside the timeout.
To check the JSON, pipe stdout through jq .. If jq complains, Claude Code will too.
Bypass it for one run
To get unblocked while you fix the hook, turn hooks off for one run:
claude --settings '{"disableAllHooks": true}'
The docs say this “takes precedence over project and local settings”. What it costs:
- It disables every hook, not just the broken one. The reference: “There is no way to disable an individual hook while keeping it in the configuration.”
- It does not disable managed hooks. Only
disableAllHooksset in managed settings can do that. - Setting
"disableAllHooks": truein your user settings does not help here, because afalsein the project’s.claude/settings.jsonoverrides it.
If the project hook guards something real, such as writes to production config, you now run without that guard. That includes any other hook-based checks, ACP’s among them. Fixing the path, permissions or interpreter usually takes a minute, so do that first.
Decide whether a broken hook blocks
A failing hook should do what you chose, not whatever the failure happens to do. Two levers.
onFailure on the hook entry. "continue" lets the action go ahead when the hook fails. "block" stops it. Use block for hooks that enforce policy, so a broken gate does not become an open door. The reference spells out the risk of the default: “A policy hook with a wrong path or a crashing script then lets everything through.”
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/policy.sh", "onFailure": "block", "timeout": 10 }
The script’s own error handling. Decide inside the script what an internal failure means, and turn it into a verdict instead of a crash. To fail closed:
#!/usr/bin/env bash
trap 'echo "policy hook failed; blocking" >&2; exit 2' ERR
set -e
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
case "$cmd" in
*"rm -rf /"*) echo "Blocked: recursive delete of /" >&2; exit 2 ;;
esac
exit 0
To fail open, use exit 0 in the trap. Exit 2 is a verdict, not a failure, so a fail-closed script blocks even on a Claude Code version too old for onFailure. Pick one per hook and write it down. A formatting hook should fail open. A secrets or destructive-command hook should fail closed.
Limits
- Permission rules. A
denyrule inpermissionsis a different mechanism with different failure modes. A hook returningallowdoes not override a matching deny rule. If a deny rule is not firing, see Claude Code deny rules not working. - Exact wording. “Repo settings” and “hook errored” are what Claude Code prints, not terms the official docs define. The behavior above follows the documented semantics, so check
/hooksto confirm the source on your version. - Hooks you cannot edit. A hook from managed settings is set by your organization. You can read it in
/hooksbut not disable it locally. - Plugin and skill hooks. They fail the same way, but the source label differs and the fix lives in the plugin or skill, not in
.claude/settings.json.
ACP’s Claude Code plugin is itself a hook, so it has this question too. If ACP cannot reach its gateway, it fails open for interactive sessions with a loud UNGOVERNED banner, and fails closed for subagent, background and API sessions. It does not use onFailure. To make interactive sessions fail closed too, run echo closed > ~/.acp/failmode (plugin 0.30.1 and later). The safety floor (force-pushes, destructive SQL through a database client, curl | sh and deletes outside the project are held for approval; rm -rf of home or root is always blocked) is checked on your machine, so it still applies during an outage.
Frequently asked questions
What does denied by pretooluse hook from "repo settings" (hook errored) mean?
A PreToolUse hook defined in the project’s settings failed to produce a verdict, and Claude Code blocked the tool call because of that failure. The hook did not decide to deny anything; it crashed, could not start, timed out, or printed invalid JSON. Fix or bypass the hook and the call goes through.
Why does a crashing hook block the call when hook errors are usually non-blocking?
By default a failed PreToolUse hook is a non-blocking error and the call continues. A failure blocks only when the hook sets onFailure to block, or on Windows without Git Bash, where a failed shell-form PreToolUse hook can block the call.
How do I test a Claude Code PreToolUse hook by hand?
| Pipe a sample tool call into the script and read the exit code: echo ‘{“tool_name”:”Bash”,”tool_input”:{“command”:”ls”}}’ | ./my-hook.sh; echo $?. Exit 0 or 2 is a verdict. Anything else, or invalid JSON on stdout, is a failure. |
How do I bypass a broken PreToolUse hook for one session?
Start Claude Code with claude –settings ‘{“disableAllHooks”: true}’. That turns hooks off for that run and takes precedence over project and local settings. It disables every hook, not just the broken one, and it cannot disable managed hooks.
Where to read more
- Claude Code hooks reference: exit codes,
onFailure, timeouts, input and output schemas - Automate actions with hooks: the troubleshooting section this page draws on
- Claude Code settings: settings files and precedence
- Claude Code hooks reference on this site: events, decisions, and what survives bypass mode
Running Claude Code? One command adds a hook. Each tool call is recorded, the session's model calls are priced, and a force-push, destructive SQL or curl | sh waits for your OK in the session.
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
irm https://agenticcontrolplane.com/install.ps1 | iex
What the installer writes · Claude Code guide · free up to 5 agents