Claude Code Hook Exit 127? Fix the Silent Failure

Claude Code Hook Exit 127? Fix the Silent Failure

Aident AI

Overlapping cobalt and coral sculptural forms reveal a narrow hidden gap with amber light.

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:

{
  "type": "command",
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh",
  "args": []
}

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:

hook_non_blocking_error
exitCode: 127
command not found
No such file or directory

You may also see all of these confusing signals:

  • /hooks lists 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; and

  • exit 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:

  1. Save your work and use a disposable branch or test repository.

  2. Record claude --version and the operating system.

  3. Identify whether the hook comes from project settings, local settings, user settings, a plugin, or managed policy.

  4. Choose one harmless allowed action and one safe fixture your policy is designed to deny.

  5. 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:

claude --debug-file /tmp/claude-hook-debug.log

Trigger one harmless action that matches the hook. After the session, inspect only the relevant lines:

grep -E 'hook_non_blocking_error|exitCode.*127|command not found|No such file' \
  /tmp/claude-hook-debug.log

On PowerShell:

claude --debug-file "$env:TEMP\claude-hook-debug.log"
Select-String -Path "$env:TEMP\claude-hook-debug.log" -Pattern `
  'hook_non_blocking_error|exitCode.*127|command not found|No such file'

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:

.claude/settings.json
.claude/settings.local.json
~/.claude/settings.json
<plugin>/hooks/hooks.json

Validate JSON before editing behavior:

jq empty .claude/settings.json

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:

{
  "type": "command",
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh"
}

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:

{
  "type": "command",
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh",
  "args": []
}

On macOS and Linux, make the script executable:

chmod +x .claude/hooks/guard.sh

For a PowerShell hook on Windows, call the executable directly and put the script path in args:

{
  "type": "command",
  "command": "powershell.exe",
  "args": [
    "-NoProfile",
    "-ExecutionPolicy",
    "Bypass",
    "-File",
    "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.ps1"
  ]
}

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:

#!/bin/sh

if ! command -v jq >/dev/null 2>&1; then
  printf '%s\n' 'Blocked: jq is required by the guardrail but is unavailable.' >&2
  exit 2
fi

GUARD_PATH="${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh"

if [ ! -x "$GUARD_PATH" ]; then
  printf '%s\n' "Blocked: guardrail is missing or not executable: $GUARD_PATH" >&2
  exit 2
fi

exec "$GUARD_PATH"

Save it as .claude/hooks/guard-wrapper.sh, make it executable, and register it in exec form:

{
  "type": "command",
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-wrapper.sh",
  "args": []
}

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:

hook matched
hook process launched
hook completed without hook_non_blocking_error
allowed action followed the normal permission flow

Then unit-test the guard with a fixture it only parses and never executes:

printf '%s\n' \
  '{"tool_name":"Bash","tool_input":{"command":"YOUR_SAFE_BLOCK_FIXTURE"}}' \
  | ./.claude/hooks/guard-wrapper.sh
printf 'exit=%s\n' "$?"

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 hook_non_blocking_error or exit 127 remains

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 /hooks as a runtime health check

Confirm one launch in the debug log

Leaving a project path unquoted in shell form

Use exec form with args

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 || true to hide failures

Handle expected skip and failure paths explicitly

Piping a hook through tee

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 PostToolUse can prevent an action

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:

Follow https://aident.ai/SETUP.md

Then ask:

Check my Aident Loadout account authentication and Vault status.
Search the staging capability catalog for a read-only GitHub issue Action.
Inspect its input schema, then use it to search anthropics/claude-code for
issue 81458. Return the canonical Action name, normalized inputs, execution
status, and result URL. Do not write to GitHub, change a connection, or reveal
a credential

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

Refresh this guide when Claude Code changes launch-failure visibility, exit-code semantics, or the exec-form hook contract.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

Plug your entire stack into your AI agents.

Plug your entire stack into your AI agents.

Plug your entire stack into your AI agents.

Skip the integration headache. Plug 750+ tools into Claude Code, Codex, and OpenClaw in one go, and let your agents execute today.

Skip the integration headache. Plug 750+ tools into Claude Code, Codex, and OpenClaw in one go, and let your agents execute today.