Back to BlogSecurity

Claude Code Hooks: Enforce Tool Calls Before Execution

Install and test a Claude Code PreToolUse hook with a harmless command and denied fixture read, then verify your session loads the guardrail.

4 min read
by APort

Summary: Claude Code hooks give developers a place to reject a proposed tool call before execution. For a team worried about secret reads or unexpected commands, start at the tool boundary: keep its built-in permissions, then use a PreToolUse hook to check proposed actions against an agent passport and policy. The example below installs APort, permits a harmless command and denies a sensitive file read without executing either tool.

Claude Code documents PreToolUse decision control. A hook can return a permission denial before the proposed tool runs. APort uses that boundary; it does not replace sandboxing, branch protection or least-privilege credentials.

Tested local setup

Prerequisites: macOS or Linux, Bash, Node.js 18 or newer, npm/npx and jq. Network access is needed to download the package. This example was tested with @aporthq/[email protected] on September 30, 2026. It creates a temporary config directory and local passport; no hosted account or API key is needed.

Run the whole block in Bash. The payloads describe proposed tool calls. The test invokes the hook directly, so it does not read a real secret or run a shell tool.

bash
set -euo pipefail
export APORT_CLAUDE_CODE_CONFIG_DIR="$(mktemp -d)"
test -d "$APORT_CLAUDE_CODE_CONFIG_DIR"
export APORT_CONFIG_DIR="$APORT_CLAUDE_CODE_CONFIG_DIR"
export APORT_OWNER_EMAIL="[email protected]"
npx --yes @aporthq/[email protected] claude-code \
  --mode=local --non-interactive --enforcement=enforce

# Confirm the installer registered a PreToolUse hook.
jq -e '.hooks.PreToolUse | length > 0' \
  "$APORT_CONFIG_DIR/settings.json"
aport_hook="$APORT_CONFIG_DIR/aport/runtime/bin/aport-claude-code-hook.sh"

# A safe proposed command returns no blocking output.
aport_allow=$(printf '%s\n' \
  '{"tool_name":"Bash","tool_input":{"command":"pwd"}}' | bash "$aport_hook")
test -z "$aport_allow"

# This path is only an input to policy evaluation, never a real file read.
printf '%s\n' \
  '{"tool_name":"Read","tool_input":{"file_path":"/tmp/.env.aport-smoke-test"}}' \
  | bash "$aport_hook" > "$APORT_CONFIG_DIR/denied.json"
jq -e '.hookSpecificOutput.permissionDecision == "deny" and
  (.hookSpecificOutput.permissionDecisionReason | contains("oap.blocked_pattern"))' \
  "$APORT_CONFIG_DIR/denied.json"

Expected result: both jq assertions print true, the shell exits successfully, and denied.json contains permissionDecision: deny with reason oap.blocked_pattern. A hook process exiting zero does not mean the tool is allowed: Claude Code reads the structured denial. Inspect the local aport/audit.log under the temporary config directory for the evaluated actions.

Connect the hook to your Claude Code session

The isolated test proves the package's installer and hook behavior. It does not prove your current Claude Code session has loaded that hook.

For your normal setup, open a fresh terminal without the temporary config overrides and run:

bash
npx --yes @aporthq/[email protected] claude-code
npx --yes @aporthq/[email protected] mode claude-code --enforcement=warn

Choose a local passport or hosted setup as appropriate. Review the generated settings, restart Claude Code and open /hooks to confirm APort is registered. Create a harmless fixture named .env.aport-smoke-test containing only dummy text. Ask Claude Code to read that fixture and check the warning. Never use a real secret for this test.

In warn mode, a policy denial is recorded but the tool can continue. Switch to enforce only after reviewing your policy, then repeat the fixture test and confirm Claude Code blocks the read. Keep the default permissions and sandbox controls enabled. See the Claude Code security guide for the host's own controls.

What to scope before a team rollout

Review file roots, command rules and MCP server allowlists for the work your developers actually do. The MCP setup example explicitly narrows the default server allowance and proves that an unapproved server is denied.

A local passport is a developer-controlled file. For centralized decisions and audit, configure hosted verification, protect the installed hook from unauthorized changes and confirm every tool path reaches it. Test allowed calls, denied calls and verification failures in the environment you plan to deploy.

The security boundary review covers policy ownership, alternate execution paths and network limits. The decision-versus-enforcement contract explains what report-only findings prove.

Install my guardrail using the Claude Code setup path, then repeat the fixture test in the session that will do the work.

Frequently Asked Questions

Common questions about this topic.

Does the example change my normal Claude Code settings?

No. It sets APORT_CLAUDE_CODE_CONFIG_DIR to a fresh temporary directory and invokes that installed hook directly. Production setup is a separate step.

Does a local test create hosted signed decisions?

No. This example uses a local passport and local audit log. Hosted verification and organization audit require hosted setup.