Anthropic documents exit code 2 as a blocking Hook error; other nonzero exit codes do not have the same blocking behavior. The Hooks reference makes the result contract explicit. So don’t start by reinstalling Claude Code: first verify the settings file and event matcher, then check the remote account and script environment, and finally validate the Hook’s input, output, and exit status. Hooks can enforce predictable checks, but they are not a replacement for operating-system permissions or CI acceptance.
This guide is for developers who can’t see a formatter, test, or command guard run on a remote Mac; DevOps engineers checking whether Hook results support Xcode build evidence; and platform engineers managing settings across shared accounts.
1. Locate the failure before changing configuration
Treat “the Hook didn’t work” as a symptom, not a diagnosis. There are four distinct failure points:
- The event didn’t fire: The action you performed did not produce the event you configured.
- The matcher didn’t match: The event occurred, but the Hook’s filter excluded the tool call.
- The process didn’t start or finish: The command was selected but failed on the remote host.
- The result had no intended effect: The script ran, but its output or exit status did not express the decision you expected.
These look similar in the editor. They need different fixes. Reinstalling Claude Code or deleting settings before you know the failure point can remove useful evidence and leave the cause untouched.
Reproduce one operation with a small, non-sensitive test file. For example, make one controlled edit and observe whether the expected formatter Hook runs. Record the action, the active account, the project directory, and any session or Hook output available. Anthropic’s Hooks documentation describes Hook debugging and logging; use that evidence to establish whether execution reached the event, the command, or the response stage.
A remote Mac adds extra variables. An interactive SSH shell may load a different startup configuration from a service or CI process. The checkout may be under another user’s home directory. A script may start from a different working directory. Any one of these can make a working local Hook appear broken remotely.
Keep the first test narrow: one event, one tool call, one expected result.
2. Check which settings scope the remote session reads
A Hooks configuration only helps if the running Claude Code process can see it. Start with the account that launches the process, not the account you use on your workstation. Then confirm the project root and which settings sources are present.
Claude Code documents user, project, local project, and managed settings. Their availability and intended scope differ; local workstation settings do not automatically travel to a separate remote account. Use the settings reference to check current file locations and behavior rather than copying a path from another machine.
| Settings source | Scope to verify | Remote Mac check |
|---|---|---|
| Managed settings | Organization-managed policy | Confirm whether the node or account receives the managed configuration. |
| User settings | The home directory of the launching user | Check the actual service or SSH account’s settings, not your local account’s file. |
| Project settings | Shared project configuration | Confirm the remote checkout contains the expected project settings file. |
| Local project settings | Local to a project checkout or user workflow | Check the remote checkout separately; do not assume this file exists there. |
Use a short comparison before editing anything:
- SSH works, CI fails: Compare the SSH user with the CI service account, including home directory, checkout path, and settings visibility.
- One project works, another fails: Check the project root and whether both checkouts contain the intended project configuration.
- It works after a manual launch only: Check how the service starts Claude Code and whether that launch context loads the expected account environment.
- A setting appears present but has no effect: Confirm the active settings source and any managed policy before assuming that another file takes precedence.
Don’t copy a developer’s whole home directory to make the Hook work. That can expose credentials or unrelated personal settings. Transfer only the intended project configuration, and keep secrets out of shared settings. For an account or environment mismatch, a focused remote Mac development environment review can help you map which user and checkout are involved before you change the node.
3. Match the configured event to the action you actually performed
A Hook event describes a point in the Claude Code lifecycle. Events are not interchangeable. A Hook attached to PreToolUse is not a substitute for one attached to PostToolUse, and neither automatically means that a user turn has ended. Events such as Stop serve a different purpose from tool-call events. Check the current event and matcher definitions before changing the configuration.
Then reduce the matcher to a single expected tool. A matcher intended for a shell command won’t necessarily match a file-editing tool. Likewise, an extra filter can exclude a call even when the event name is right. Inspect the event-specific input and match against the tool name and fields the documentation describes; don’t infer a match from the command you meant Claude Code to run.
Use this sequence to isolate the condition:
- Pick one harmless operation that should trigger the Hook.
- Confirm which event that operation should produce.
- Temporarily test one matcher and one tool, without unrelated filters.
- Check whether the command starts and whether its diagnostic marker appears.
- Restore necessary filtering one condition at a time, repeating the same operation.
If the Hook starts after simplifying the matcher, add the original conditions back individually. The condition that stops the match identifies the fault more reliably than a broad matcher that appears to “fix” the issue but runs on unintended calls.
For command restrictions, distinguish a Hook’s tool-call decision from Claude Code’s permission configuration. The permissions reference describes permission controls; a Hook can contribute a decision at its documented event, but it does not grant or revoke operating-system access. Avoid treating a matcher as a host-level security control.
4. Compare the remote script environment with the one that works
When a Hook matches but its command fails, compare the execution environment instead of changing the event. Start with the executable bit, interpreter, absolute command paths, required dependencies, working directory, and file permissions. Check these from the same account and launch method that runs Claude Code.
A common trap is relying on an interactive shell’s PATH. Your SSH session may load shell startup files that a service account or CI runner does not. A command available after login can therefore be missing from a Hook process. Check the actual environment without printing secrets. If a dependency is required, verify its path explicitly or establish the required environment in the script.
Also check:
- Whether the Hook script is executable and its interpreter exists at the configured path.
- Whether referenced files are relative to the directory the Hook actually uses.
- Whether the launching account can read inputs and write intended outputs.
- Whether a dependency is installed for that account, rather than only for another user.
- Whether logs identify the event and outcome without exposing credentials.
Use a temporary diagnostic that writes a harmless marker and a small set of environment facts to a protected location. Don’t dump the full environment: it may contain tokens or signing credentials. Remove the diagnostic after identifying the mismatch, and avoid recording sensitive Hook input.
If the command works in an interactive SSH terminal but fails under a runner, treat those as separate execution environments until proven otherwise.
5. Verify what the Hook returned, not just whether it ran
A process starting is not the same as a decision taking effect. Claude Code Hooks receive event input and use documented output and exit behavior. Check that your script reads the expected input, emits valid output where required, and returns the intended exit status. Consult the Hook input and output contract before changing the script to “make it block.”
For a controlled test, separate these outcomes:
- No decision requested: The Hook ran but did not return a documented decision for that event.
- Explicit block or other decision: The Hook returned the event-appropriate decision in the expected structure.
- Script error: The process failed, produced malformed output, or returned an error status.
- Later-stage failure: The Hook completed, but a tool operation or CI step failed afterward.
These outcomes require different responses. An empty or malformed response does not prove that a command was allowed or denied. A nonzero status also does not automatically mean the same thing for every exit code; Anthropic documents the distinction between blocking and non-blocking Hook errors. Use a harmless test action to confirm both the process result and the visible effect in Claude Code.
Keep standard output, standard error, and exit status distinct in your diagnostic approach. Avoid mixing debug text into structured JSON output. If you need a trace, send safe diagnostic details to an appropriate log, and verify that the script’s final output still conforms to the documented schema.
For command approvals and file access, maintain a separate policy check. The Claude Code permissions guide explains the permission layer. A Hook that rejects a particular tool call is not equivalent to restricting the remote account’s filesystem, network, or other host capabilities.
6. Validate Xcode checks separately and choose the right gate
A Hook can start a formatter or an Xcode test command, but a successful Hook invocation alone is not evidence that the intended test suite passed. The script must run the correct command, preserve its result, and make that result available to the process or CI job that owns acceptance.
Apple’s documentation explains how to run tests and interpret Xcode results and documents the Xcode command-line tools. For remote execution over SSH, Apple also provides an automation guide covering SSH and Xcode testing. Use those sources to confirm the intended test action and result material; don’t treat a Hook’s log line as an Xcode test report.
Separate the checks by purpose:
- After a file change: Run formatting or a quick deterministic check when that is the intended Hook responsibility.
- At session end: If a
StopHook runs tests, make the script execute the test command and return or record its actual result. TheStopevent itself does not certify that Xcode tests passed. - In CI: Preserve the build or test job’s exit status and result artifacts as the formal acceptance evidence. Keep Hook output as diagnostic context, not as the only release gate.
Use a controlled acceptance sequence before relying on the setup: confirm a valid change triggers the expected check; confirm a deliberately failing check is reported as a failure; inspect the test result evidence; and repeat after the service or node restarts. If a Hook fails after restart, recheck the launching account and environment rather than assuming the project configuration disappeared.
Keep the trade-off explicit.
- Hooks are useful for fast, repeatable checks close to a tool call.
- CI is better suited to recorded build and test acceptance across a controlled workflow.
- Operating-system permissions and human approval still define separate security boundaries.
- A Hook should not be your only control for destructive commands or access to sensitive files.
If your acceptance needs include Xcode automation, compare a remote Mac mini option with your current setup only after defining the account, test command, and evidence you need. If you cannot identify the exact Hook event and returned result, fix that observability gap before moving a release gate.
Frequently asked questions
Why might Claude Code Hooks not run on a remote Mac?
First check which settings file the active account loaded, then confirm that the configured event matches the action you performed. A Hook can also fail before its command starts because the matcher does not fit, or start and fail because the remote shell lacks a dependency, permission, or expected working directory. Capture session evidence before changing configuration.
Which Hooks settings file does Claude Code use remotely?
It depends on the account and project context. Check managed settings, user settings under that account, shared project settings, and project-local settings separately. A file in your workstation account is not automatically available to an SSH or CI service account. Inspect the actual checkout path and launch identity before assuming a setting was loaded.
How can I tell whether a PreToolUse matcher failed to match?
Reduce the test to one tool call and one PreToolUse Hook. Confirm the event name, matcher value, and tool name shown in the Hook input or session evidence. If the command never starts, inspect the event and matcher first; if it starts, move on to shell errors and output handling rather than broadening the matcher blindly.
What should I check when a Hook script cannot find a command over SSH?
Compare the SSH shell with the environment used to start Claude Code. Check the executable path, PATH, interpreter, working directory, and access to required files. Prefer a verified absolute path or explicitly establish the needed environment in the script. Keep diagnostic output free of tokens, signing credentials, and other secrets.
Can a Stop Hook verify that Xcode tests passed?
A Stop Hook can run a test command, but the event alone is not proof that the tests passed. The script must execute the intended Xcode test action, preserve its exit status, and retain the result evidence. For release or CI acceptance, use the CI job and Xcode test results as the formal gate, not an unverified Hook notification.
If your current approach depends on a non-macOS host, it cannot provide the same macOS-specific Xcode execution environment; if it relies on an interactive SSH shell, its environment may differ from the runner; and if it uses a shared account, settings and permissions can be harder to isolate. When you need a real macOS environment to reproduce and validate the issue, renting a Mac through MACCOME can be a more direct fit than buying hardware for a temporary project. Keep CI as the formal test gate, and choose rental only when remote Mac access fits your project duration and workflow.