Aident AI

Claude Code Hook Exit 127? Fix the Silent Failure
If a Claude Code hook records exit 127, the hook command did not launch. For a PreToolUse guardrail, that can mean the tool call continues even though /hooks still lists the guardrail as configured. Start Claude Code with a known debug-log path, confirm the launch failure, then move path-based hooks to exec form by adding an args array. Use a small wrapper that returns exit code 2 when a required guardrail dependency is unavailable.
The shortest safe fix is usually this change:
With args present, Claude Code resolves command as an executable and passes each argument without shell tokenization. A project path containing a space stays one path. Do not treat this as proof that the policy itself is correct. After the launch fix, verify one allowed fixture and one safely blocked fixture.
Match the Exact Failure
This guide applies when one or more of these signals appear together:
You may also see all of these confusing signals:
/hookslists the hook;the script exists and is executable;
the script behaves correctly when you invoke it by hand; and
Claude Code continues the matched action.
The configuration can be valid while the command still fails at launch. In issue 81458, an unquoted project path containing a space caused eleven PreToolUse hooks to miss 6,865 invocations in one session. The session recorded exit 127 each time, but the tool calls proceeded.
Use a different diagnosis when the debug log shows no matcher hit, invalid JSON output, a timeout, or an intentional exit code from a running hook. Those failures can share the symptom "my hook did nothing," but they have different fixes.
Why Exit 127 Does Not Block the Tool
Exit 127 conventionally means the shell could not find or launch the command. Claude Code's hook contract is more specific than normal shell success and failure:
exit 0 means the hook completed and Claude Code may process its output;
exit 2 blocks only on hook events that support blocking, including
PreToolUse; andexit 1 and other nonzero codes are non-blocking errors for most events.
That means "the guardrail crashed" is not the same as "the guardrail denied the action." A PostToolUse hook also cannot undo a tool call that has already happened. Put preventive policy on an event that can block, and make its failure path explicit.
Prerequisites
Before changing hook configuration:
Save your work and use a disposable branch or test repository.
Record
claude --versionand the operating system.Identify whether the hook comes from project settings, local settings, user settings, a plugin, or managed policy.
Choose one harmless allowed action and one safe fixture your policy is designed to deny.
Do not test with a real destructive command, production credential, or production repository.
Expected result: you can compare launch behavior before and after the fix without weakening the policy or risking real data.
Step 1: Capture the Hook Debug Log
On macOS or Linux, start a fresh session with a log file outside the repository:
Trigger one harmless action that matches the hook. After the session, inspect only the relevant lines:
On PowerShell:
Expected result: the log identifies the matched hook, command, exit code, and stderr. If the log shows no match, inspect the event and matcher before changing the command path. Claude Code's configuration guide notes that matchers are case-sensitive and that hooks belong in settings.json, not a standalone project hooks.json.
Debug logs can contain command input and output. Keep the file local, redact it before sharing, and delete it when the investigation is complete.
Step 2: Find the Configuration Source
Run /hooks inside Claude Code and note the source of the failing entry. Common sources include:
Validate JSON before editing behavior:
Expected result: the file parses, and the hook source you edit is the source Claude Code actually loaded. Remember that project-local settings can override broader settings. A hook shown as configured is not yet proven to have launched.
Step 3: Replace Shell Path Parsing With Exec Form
This shell-form configuration is fragile when the project path contains spaces:
Claude Code passes shell-form commands through sh -c on macOS and Linux. The shell expands the variable and splits the unquoted path.
Prefer exec form for a path-based hook:
On macOS and Linux, make the script executable:
For a PowerShell hook on Windows, call the executable directly and put the script path in args:
Expected result: the command launches even when the project root contains spaces or shell metacharacters. Exec form also avoids an inline pipeline accidentally changing which process supplies the final exit code.
If the hook genuinely needs pipes, redirects, or &&, keep that shell logic inside one reviewed wrapper script. Register the wrapper itself in exec form.
Step 4: Make Missing Dependencies Fail Closed
Fixing the launch path does not protect against a missing interpreter or helper. For a blocking PreToolUse policy, use a small stable wrapper as the registered hook:
Save it as .claude/hooks/guard-wrapper.sh, make it executable, and register it in exec form:
Expected result: a missing required dependency produces exit 2 and visible stderr on a blocking event instead of exit 1 or 127. The final exec preserves stdin and returns the guard script's own exit status.
Do not blindly convert every hook failure into exit 2. Notification, setup, post-action, and cleanup hooks have different decision semantics. Use this pattern only where blocking is intended and supported.
Step 5: Prove the Hook Runs and Blocks
Restart Claude Code after the configuration change, again with a known debug file. First trigger the harmless allowed action.
Expected result:
Then unit-test the guard with a fixture it only parses and never executes:
Expected result: the wrapper returns exit 2 and prints the policy reason to stderr. Finally, trigger the equivalent safe fixture through Claude Code in the disposable repository and confirm that the tool call is blocked.
Replace YOUR_SAFE_BLOCK_FIXTURE with a test token your guard explicitly recognizes. Do not paste a real destructive command just to prove the hook can stop it.
Step 6: Add a Health Check for Important Guardrails
For a guardrail that protects commits, secrets, infrastructure, or destructive commands, test these conditions after installation and updates:
Check | Expected result |
|---|---|
Project path contains a space | Hook still launches |
Required helper is absent | Blocking event returns exit 2 with a reason |
Allowed fixture is submitted | Normal permission flow continues |
Denied fixture is submitted | Tool call is blocked |
Hook config is moved to the wrong file | Health check fails visibly |
Debug log is inspected | No |
Repeat the check when Claude Code, the plugin, the hook runtime, or its dependencies change. A Reddit report published in the same week as issue 81458 described a plugin losing roughly 45% of activations to silent hook paths, including a PATH mismatch and a Windows interpreter stub. That is social evidence of the same operational question, not proof that every hook has the same failure.
Common Failure Modes
Failure | Safer response |
|---|---|
Trusting | Confirm one launch in the debug log |
Leaving a project path unquoted in shell form | Use exec form with |
Testing the script only by hand | Test through Claude Code's actual hook process |
Returning exit 1 from a blocking guardrail | Return exit 2 with a clear stderr reason |
Adding | Handle expected skip and failure paths explicitly |
Piping a hook through | Keep the registered wrapper in exec form and preserve status |
Checking only that an interpreter exists | Execute a harmless runtime probe during installation or health check |
Running a real destructive test | Use a test-only denied fixture in a disposable repository |
Assuming | Put preventive policy on a supported blocking event |
Why This Fix Works
The change separates two contracts that shell form mixes together. Exec form resolves one executable and passes each argument as an atomic value, so an ordinary project path cannot be split into a different command. The wrapper then turns missing prerequisites into the only blocking exit code Claude Code recognizes for PreToolUse.
The two-fixture test proves more than configuration presence. The allowed fixture proves the hook launches without disrupting normal work. The denied fixture proves its decision reaches Claude Code and changes the tool outcome. This follows the repeatable structure behind Aident's Ollama networking guide: use the exact error, make one boundary change, state the expected result, and verify the effect rather than trusting configuration.
Hooks are still one layer. Use Claude Code permissions, sandboxing, repository protections, and scoped credentials for defense in depth. For external APIs, keep raw provider keys out of the agent prompt and verify the actual integration boundary separately.
Verify One Read-Only Action Outside the Hook Boundary
Set up Aident Loadout by pasting:
Then ask:
Expected result: one schema-inspected read-only execution, one matching issue URL, zero provider writes, and no credential copied into the prompt. This does not replace a local Claude Code guardrail. It gives you a separate, measurable integration check whose success is not inferred from /hooks.
Sources
Claude Code hooks reference, Anthropic, accessed August 2, 2026
Debug your Claude Code configuration, Anthropic, accessed August 2, 2026
Hook launch failures are silent and non-blocking, issue opened July 26, 2026
Silent hook failures cost me roughly half my plugin activations, Reddit post published August 1, 2026
Refresh this guide when Claude Code changes launch-failure visibility, exit-code semantics, or the exec-form hook contract.


