ection**: PreToolUse intercepts tool invocations before execution. Stop fires when the model completes its reasoning turn. These two events cover security interception and workflow signaling without overlapping responsibilities.
2. Execution Environment: Shell scripts with jq for JSON parsing provide cross-platform compatibility, minimal dependency footprint, and deterministic exit code semantics. TypeScript/Node alternatives exist but introduce runtime overhead and version fragmentation.
3. Pattern Matching Strategy: Regex-based path and variable matching outperforms exact-path whitelisting in dynamic projects. We use anchored patterns with explicit exclusion lists to prevent false positives on legitimate configuration files.
4. Notification Routing: Desktop notifications require OS-specific dispatchers. macOS uses osascript, Linux/WSL uses notify-send. The hook abstracts this behind a runtime detection layer.
Implementation: Security Interceptor
The following script intercepts Read and Bash operations, evaluates file paths and environment variables against a configurable deny list, and returns a structured allow/deny decision.
#!/usr/bin/env bash
set -euo pipefail
# workspace-guard.sh
# Intercepts PreToolUse events to block access to sensitive paths and variables
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
INPUT_PATH=$(echo "$INPUT" | jq -r '.tool_input.path // empty')
INPUT_CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Default deny patterns (customizable via environment variables)
DENY_PATHS="${GUARD_DENY_PATHS:-\.env$|\.env\.[a-z]+$|credentials\.json|aws-credentials|ansible-vault|\.pem$|\.key$}"
DENY_VARS="${GUARD_DENY_VARS:-TOKEN|PASSWORD|SECRET|API_KEY|JWT|PRIVATE_KEY}"
BLOCKED=false
REASON=""
# Evaluate file path access
if [[ -n "$INPUT_PATH" ]]; then
if echo "$INPUT_PATH" | grep -Eqi "$DENY_PATHS"; then
BLOCKED=true
REASON="Path matches sensitive pattern: $INPUT_PATH"
fi
fi
# Evaluate command execution for credential exposure
if [[ -n "$INPUT_CMD" ]] && [[ "$TOOL_NAME" == "Bash" ]]; then
if echo "$INPUT_CMD" | grep -Eqi "cat.*\.env|env\s+|printenv|aws\s+configure|vault\s+read"; then
BLOCKED=true
REASON="Command attempts to expose credentials or environment state"
fi
fi
# Evaluate environment variable references in tool input
if echo "$INPUT" | grep -Eqi "$DENY_VARS"; then
BLOCKED=true
REASON="Tool input contains restricted credential pattern"
fi
# Construct response
if [[ "$BLOCKED" == true ]]; then
echo "{\"allow\": false, \"reason\": \"$REASON\"}" | jq .
exit 1
else
echo "{\"allow\": true}" | jq .
exit 0
fi
Rationale:
set -euo pipefail ensures strict error handling. A failed jq parse or unset variable immediately halts execution, preventing silent allow decisions.
- Environment variable overrides (
GUARD_DENY_PATHS, GUARD_DENY_VARS) enable project-specific tuning without modifying the script.
- Separating path evaluation from command evaluation prevents false positives on legitimate
grep or find operations that don't target credential stores.
- JSON output is piped through
jq . to guarantee valid formatting, regardless of shell quoting edge cases.
Implementation: Workflow Completion Signal
The following script listens for the Stop event and dispatches a desktop notification. It includes WSL/Linux display server fallback logic and macOS native integration.
#!/usr/bin/env bash
set -euo pipefail
# turn-completion-alert.sh
# Fires desktop notification when Claude Code finishes a turn
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
TOOL_COUNT=$(echo "$INPUT" | jq -r '.tool_use_count // 0')
# Determine OS and dispatch notification
if [[ "$(uname)" == "Darwin" ]]; then
osascript -e "display notification \"Session $SESSION_ID completed ($TOOL_COUNT tools)\" with title \"Claude Code Workflow\""
elif command -v notify-send &> /dev/null; then
# Handle WSL display server or native Linux
if [[ -n "${WSL_DISTRO_NAME:-}" ]]; then
export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0
fi
notify-send "Claude Code" "Turn complete | Session: $SESSION_ID | Tools: $TOOL_COUNT" --icon=dialog-information
else
# Fallback: terminal bell + log
echo -e "\a"
echo "[ALERT] Session $SESSION_ID finished at $(date -u +%H:%M:%S)" >> ~/.claude-hooks.log
fi
exit 0
Rationale:
- WSL requires explicit
DISPLAY variable configuration to route notifications to the Windows notification center. The script dynamically resolves the WSL nameserver IP.
notify-send is wrapped in a command existence check to prevent failures on minimal containers.
- The fallback mechanism ensures visibility even in headless or restricted environments.
- Exit code
0 is mandatory for Stop events; blocking termination would break the host process lifecycle.
Pitfall Guide
1. Ignoring the stdin/stdout JSON Contract
Explanation: Hooks that print debug messages, stack traces, or unformatted text to stdout will corrupt the JSON response. Claude Code expects strictly valid JSON. Any deviation causes silent hook failure or host process termination.
Fix: Always pipe output through jq . or a JSON serializer. Redirect logs to stderr (>&2) or dedicated files. Never mix diagnostic output with the response payload.
2. Overly Broad Regex Patterns
Explanation: Patterns like .*key.* or .*secret.* will match legitimate files like public-key.pem, feature-key-config.json, or secret-manager-client.ts. This creates false positives that block development workflows.
Fix: Anchor patterns to file extensions and directory structures. Use negative lookahead where supported. Maintain an explicit allowlist for known safe paths and test patterns against a sample repository before deployment.
3. Missing Executable Permissions
Explanation: Claude Code invokes hooks as subprocesses. Without the execute bit set, the host process receives a permission denied error and falls back to default behavior, silently bypassing security controls.
Fix: Always run chmod +x .claude/hooks/*.sh after deployment. Include permission verification in your project's setup script or CI pipeline.
Explanation: Overly aggressive PreToolUse blocks can interrupt multi-step operations. For example, blocking Read on a configuration file may prevent a subsequent Edit or Bash command from executing, leaving the workspace in an inconsistent state.
Fix: Implement granular allow/deny logic. Log blocked attempts with context. Use session-scoped allowlists for temporary overrides during refactoring or migration tasks.
5. WSL/Linux Display Server Misconfiguration
Explanation: Desktop notifications fail silently in WSL2 or headless Linux environments because the D-Bus session bus or X11/Wayland display server is not accessible. Developers assume the hook failed rather than recognizing the environment limitation.
Fix: Detect WSL_DISTRO_NAME and configure DISPLAY dynamically. Provide terminal-based fallbacks (bell character, log file, or webhook). Document environment requirements in project READMEs.
6. Hardcoding Absolute Paths
Explanation: Hooks that reference /home/user/project/.env break when cloned to different directories, CI runners, or containerized environments. This destroys portability and increases maintenance overhead.
Fix: Use relative path resolution, environment variables, or project root detection (git rev-parse --show-toplevel). Design hooks to be workspace-agnostic.
7. Misinterpreting Exit Code Semantics
Explanation: Developers sometimes return exit 0 for blocked operations or exit 1 for allowed ones. Claude Code strictly interprets 0 as allow and non-zero as deny. Inverted logic creates security bypasses or workflow paralysis.
Fix: Document exit code contracts in hook headers. Implement unit tests that verify exit codes against known inputs. Use explicit variable naming (ALLOW=true/false) to prevent cognitive inversion errors.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Solo developer, low sensitivity | Prompt-based guardrails + manual review | Minimal setup, acceptable risk for non-production code | $0 |
| Team project, shared credentials | Hook-based enforcement + centralized denylist | Deterministic blocking, audit trail, reduces credential leakage risk | Low (dev time) |
| Enterprise, compliance requirements | Hook enforcement + SIEM integration + policy-as-code | Meets SOC2/ISO27001 controls, enables automated compliance reporting | Medium (infrastructure) |
| CI/CD pipeline, automated agents | Hook enforcement + headless fallback logging | Prevents credential exposure in build logs, maintains pipeline stability | Low (logging overhead) |
Configuration Template
Copy this structure into .claude/settings.json. Adjust hook paths and event bindings to match your project layout.
{
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/workspace-guard.sh"
}
]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/turn-completion-alert.sh"
}
]
}
]
}
}
Notes:
matcher uses regex to filter which tool invocations trigger the hook. .* applies to all operations. Narrow this to Read|Bash if you want to reduce overhead on Edit or Write operations.
- Multiple hooks can be chained per event. They execute sequentially; a deny in any hook blocks the operation.
- Environment variables (
GUARD_DENY_PATHS, GUARD_DENY_VARS) should be exported in your shell profile or project .env file before launching Claude Code.
Quick Start Guide
- Initialize hook directory: Create
.claude/hooks/ in your project root.
- Deploy scripts: Save
workspace-guard.sh and turn-completion-alert.sh to the directory.
- Set permissions: Run
chmod +x .claude/hooks/*.sh.
- Configure events: Add the JSON template to
.claude/settings.json.
- Validate: Open Claude Code, attempt to read a
.env file, and verify the operation is blocked with a JSON reason response. Confirm desktop notification appears when the session completes.
Hook-driven enforcement transforms AI coding assistants from open context consumers into bounded, auditable development partners. By intercepting tool execution at the OS level, you eliminate prompt dependency, reduce credential exposure, and establish a verifiable security posture that scales across teams and compliance frameworks.