Diagnosis: If Claude Code Hooks do not run on a remote Mac, check the event and matcher first, then script launch, SSH environment and working directory, and finally exit status and Hook output. Do not assume Claude Code is broken or disable permission controls to make a Hook run.
This guide is for developers using Claude Code over SSH on a remote Mac.
It also helps teams maintaining shared project Hooks and CI engineers checking macOS formatting, build, or test steps.
Identify which part of the Hook chain fails
“Not running” can describe different failures. The event may never occur, the matcher may reject the relevant tool, the command may fail to start, or the script may finish without producing the result you expected. Treat these as separate cases. Reinstalling Claude Code before you know which case applies can discard useful evidence without fixing the cause.
Before changing settings, record the Claude Code version, how the session was started, which settings file should apply, and the exact action that should trigger the Hook. Reproduce the action once, then repeat it with a harmless test Hook that leaves a visible trace. This gives you a baseline and keeps the test separate from a formatter, build, or other project operation.
| What you observe | Likely failure layer | First check |
|---|---|---|
| No visible trace from a harmless probe | Event, matcher, or settings scope | Confirm the event and matcher against the official reference |
| A trace appears, but the intended script does not run | Path, interpreter, or execution permission | Check the remote path, shebang, and file permissions |
| The command runs in SSH but not in the Hook | Environment or current directory | Compare PATH, required variables, and working directory |
| The script runs but the expected action does not happen | Script logic, exit status, or Hook response | Capture output and status, then inspect the documented response behavior |
A Hook is not a general command that runs whenever you open a project. It is tied to a supported event, with optional matching conditions and settings scope. The official Hook reference describes the event and configuration model. Use that reference for the Claude Code version you are actually running; do not infer event behavior from a different setup or an old example.
Check event matching and settings scope
Start with the operation that should cause the Hook to run. Identify which documented event corresponds to that operation, then confirm that the configured matcher fits the relevant tool or event input. A mismatch can look exactly like a missing Hook because the command is never reached.
Keep the probe minimal. For example, temporarily configure a harmless command Hook that writes one fixed line to a file in your home directory. Avoid formatting, deleting, building, or changing project files during this test. If the probe does not leave a trace, investigate event selection and settings scope before debugging the project script.
A simplified configuration shape can help you check for structural errors:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/absolute/path/to/hook-probe.sh"
}
]
}
]
}
}
Treat this as a shape to compare with the current Hook configuration reference, not as a universal configuration to paste unchanged. The event and matcher must fit your intended operation, and the command path must exist on the remote host. Use the official settings documentation to verify whether you placed the setting at user or project level and whether that scope applies to this session.
| Configuration detail | What to verify on the remote Mac | Evidence of a mismatch |
|---|---|---|
| Event | The documented event corresponds to the action you performed | The action completes without any probe trace |
| Matcher | Its value matches the relevant tool or event input | A broader probe works, but the restricted matcher does not |
| Settings file | The session reads the user or project settings you edited | A known harmless change in that file has no effect |
| Command path | The configured path resolves on the remote host | The Hook is recognized, but the command cannot be launched |
For shared projects, distinguish a project setting from a personal setting instead of assuming every developer has the same local configuration. A Hook that appears to work for one account may depend on that account’s user-level settings or files. The Hook guide provides examples of configuration and Hook behavior; use its examples to validate the structure, then adapt paths and matchers to your project.
Verify the script can start
Once the harmless probe shows that the event and configuration are being reached, check the command that should run. Verify that its path exists on the remote Mac, that the user running Claude Code can read it, and that the script is executable when the invocation requires that permission. If the script uses a shebang, confirm that the interpreter named there is installed at that path.
Run checks from the same account and host used by the Claude Code session:
whoami
pwd
ls -l /absolute/path/to/hook-script.sh
head -n 1 /absolute/path/to/hook-script.sh
These commands show the account, current directory, file permissions, and interpreter declaration. They do not prove that every dependency is available, so check any tools the script invokes as well. If a script works only when launched through a particular shell, make that invocation explicit and confirm it matches the documented command Hook format. Avoid adding broad permissions as a substitute for locating the actual path or interpreter problem.
If the Hook command is a shell script, test it directly under the same account before testing it through Claude Code. That separates a shell or filesystem failure from a Hook event failure. Keep the test safe: use a diagnostic script that records a short message, and do not use an unreviewed command copied from a log or project issue.
Compare the SSH and Hook environments
An interactive SSH terminal and a process started by Claude Code do not necessarily have identical startup conditions. Shell startup files may differ. A PATH entry added only for interactive use may be missing from the Hook. The current directory may be the project root in one context and another directory in the other. These differences explain why a command can succeed when typed manually but fail during a Hook.
From the SSH terminal, inspect the environment values your script actually needs:
printf 'PATH=%s\n' "$PATH"
pwd
command -v brew
command -v git
Then use a harmless temporary diagnostic Hook to record the corresponding PATH and current directory from the Hook process. Do not dump the entire environment to a shared log: it may contain credentials or other sensitive values. Check only the variables and executable locations required by the script.
The official environment variable reference helps you distinguish supported Claude Code variables from assumptions about shell inheritance. If a required tool is installed outside the Hook’s PATH, use an explicit executable path or set a narrowly scoped PATH in the script. If the script reads project files, resolve paths from a known project root rather than relying on whichever directory happens to be active.
For a remote Mac development environment, verify that Homebrew, project scripts, and the macOS toolchain are installed on the remote host, not merely on your laptop. A local terminal’s successful xcodebuild or formatter invocation does not establish that the remote session can find the same executable. If you are designing the access setup as well as debugging it, use the remote Mac deployment information for Singapore or Japan to review the available deployment context; these pages do not replace checking the actual PATH and working directory in your session.
Read output, exit status, and debug information
When the script starts but the task still has no visible effect, inspect what it returned. Capture standard output, standard error, and the process exit status separately. A successful shell status does not guarantee that the intended file changed or that Claude Code will display the text as a conversational response. Conversely, a nonzero status can identify a script failure even when the Hook itself was invoked.
For a temporary diagnostic, direct output to a private log file owned by the account running the session. Keep the record narrow:
{
printf 'hook reached\n'
printf 'directory: %s\n' "$PWD"
printf 'PATH: %s\n' "$PATH"
} >> "$HOME/claude-hook-probe.log" 2>&1
Do not log full Hook input or environment contents unless you have reviewed them for secrets and have a clear reason to retain them. Remove diagnostic logging when the test is complete, especially if the Mac is shared by a team.
Use the supported configuration debugging process rather than guessing which settings Claude Code loaded. The official configuration debugging documentation explains how to inspect configuration issues. The Hook reference also documents Hook input and output behavior, including distinctions between command execution and Hook responses. Check the relevant event’s documented behavior before expecting output to appear in the conversation.
If logs show that the command was invoked, the problem has moved beyond “Hook not triggered.” Check script arguments, assumptions about input, file ownership, and any downstream command’s own output. If the command was not invoked, return to event matching, configuration scope, or command launch rather than editing the script’s business logic.
Validate the fix without widening permissions
After the probe works, replace it with the real project action in stages. Start with a read-only check, then try a formatter or test that has predictable effects, and only then validate a build or other task that changes generated files. Review the working tree and logs after each action. This makes it easier to tell whether the Hook ran and whether its side effect came from the expected command.
Do not solve a Hook problem by disabling permission controls or granting unrestricted access to scripts. A Hook can run commands with access to the remote account and project files. Review the command, executable path, input handling, and affected files before enabling it for a shared project. The Claude Code security guidance and permission documentation explain the security boundary and permission controls; keep those controls in place while you diagnose the execution chain.
Before calling the issue resolved, confirm that the Hook runs in the intended settings scope, uses the expected remote account, finds every required command, and produces an observable result. Also verify that a failed check does not delete files, expose credentials, or silently change unrelated project state. A one-off successful probe is evidence that the path works under that session, not proof that every team member has identical settings.
Frequently asked questions
Why can a Hook be in settings but never trigger?
Presence in a file does not prove that the current session loaded that file or that its matcher fits the action. Confirm the documented event, test a harmless broad probe, and check user-versus-project settings scope. Then narrow the matcher again. This sequence distinguishes a configuration loading problem from a matcher that simply excludes the operation you expected to catch.
Why does SSH find a command that the Hook cannot find?
Your interactive shell may add directories or variables through startup files that are not present for the Hook process. Compare PATH and the working directory inside both contexts. Check the exact executable with command -v in SSH, then use a diagnostic Hook to inspect only the values it needs. Prefer an explicit path or a small script-level environment definition over changing global shell configuration.
What should I check in a Hook script's permissions and working directory?
Check that the configured path exists on the remote host, that the Claude Code account can read and execute it, and that its shebang names an available interpreter. Print the current directory during a harmless probe. If the script uses relative paths, anchor them to a known project location. Do not make the file world-writable or grant broader access just to make the test pass.
Where should I look when a Hook runs but has no visible effect?
Capture standard output, standard error, and exit status, then enable the supported configuration debugging output. Check whether the event sends a response into the conversation or records details only in logs. If the process launched successfully, inspect the script’s own assumptions and downstream command results. Avoid retaining raw Hook input or complete environment dumps, since those can expose sensitive project data.
If your current setup is a Linux-only host, it cannot supply macOS-specific tools; if you rely on a local Mac, that machine remains tied to your hardware budget and maintenance. First verify the problem with the diagnostic path above. If your project needs a remotely accessible macOS toolchain for temporary testing or build work, renting a Mac through OakVM can provide that execution environment without turning a Hook issue into a permissions workaround. Use the Mac deployment comparison to assess whether a remote Mac fits your workload; for a long-running, stable workload or a requirement for physical peripherals, compare that option with owning and maintaining a local Mac.
Frequently asked questions
Why does a Claude Code Hook fail to trigger even though the settings file contains it?
A Hook can be present in a settings file without matching the event or tool involved in your action. Check the documented event name, the matcher value, and whether the setting applies at the user or project level. Then run a harmless probe and use Claude Code's configuration debugging guidance to confirm the active configuration.
Why can a command work in my SSH terminal but fail in a Claude Code Hook?
The Hook process may not inherit the same shell startup files, PATH, environment variables, or working directory as your interactive terminal. Compare those values from inside a harmless diagnostic Hook, then use explicit executable paths where appropriate. Do not assume that a command available in your terminal is available to every process started in that remote session.
How should I check a Hook script's permissions and working directory on a remote Mac?
Connect to the same Mac and verify that the configured path exists, the file is readable and executable for the account running Claude Code, and its shebang points to an installed interpreter. Also print the Hook's current directory during a safe probe. If the script depends on project-relative files, resolve their paths explicitly rather than relying on an assumed launch directory.
Where can I find logs when a Claude Code Hook runs but produces no visible result?
Start by capturing the Hook's standard output, standard error, and exit status without logging secrets or full input payloads. Then enable the supported Claude Code configuration debugging output and check whether the selected event sends a response into the conversation or records details only in logs. The official Hook and debugging references describe the relevant output behavior.
Run Your Remote Mac Workflows on a Dedicated OakVM Node
Use a dedicated Apple Silicon Mac Mini to test your remote development workflows in a consistent environment.
