In mid-2024, Cloudflare's internal platform team began piloting AI-assisted coding agents for their edge infrastructure. A senior engineer, Paulo Frazão, described a near-miss in a public retrospective: an AI agent had been given broad file-write permissions to update configuration templates. One afternoon it modified a production YAML file that controlled routing rules for over two million domains. The change was syntactically valid. The agent had no reason to pause. There was no hook to intercept the write and check whether the target path was in a protected namespace. The file was caught fifteen minutes later by a human reviewer before deployment — but the incident prompted the team to implement pre-execution hooks that classified every file path before any write was allowed to proceed.
Frazão later noted: "The agent wasn't wrong. It did exactly what we told it to do. We just hadn't told it everything it needed to know about context."
Claude Code's hook system is a programmable interception layer that runs shell commands or scripts at defined points in Claude's execution lifecycle. Hooks are declared in your settings.json (at the project or user level) and fire automatically — Claude itself does not decide whether they run. That distinction is critical: hooks are enforced by the framework, not negotiated with the model.
There are two primary hook positions. A PreToolUse hook fires before Claude executes any tool call — file writes, bash commands, web fetches, anything. A PostToolUse hook fires immediately after a tool call completes, before Claude continues processing the result. This bracketing gives you two distinct moments of control: one to decide whether to allow the action, one to verify what the action produced.
Hooks are configured in the hooks key of your Claude Code settings file. Each hook entry specifies a matcher — which tool or tool pattern triggers it — and a command — the shell string to execute. The command receives context about the tool call via environment variables or stdin (depending on configuration).
A minimal PreToolUse hook that logs every file write looks like this in settings.json:
"hooks": { "PreToolUse": [{ "matcher": "Write", "hooks": [{ "type": "command", "command": "echo \"$CLAUDE_TOOL_INPUT\" >> /var/log/claude-writes.log" }] }] } — The matcher "Write" targets the Write tool specifically. Using "*" matches all tools. The command receives the full JSON tool input via the CLAUDE_TOOL_INPUT environment variable.
A hook command that exits with status code 0 is treated as a pass — Claude continues normally. A hook that exits with any non-zero status code blocks the tool call. Claude receives a message that the action was blocked, and it can attempt an alternative approach or report back to the user. This design means any existing Unix tool or script can function as a gate: a Python validator, a regex filter, a database lookup, or a simple grep.
This is exactly what Cloudflare's team implemented after Frazão's incident: a pre-write hook that ran a path-classification script. Paths matching production namespaces (/etc/, /var/cf-config/, specific S3 bucket patterns) caused the hook to exit 1, blocking the write and logging the attempt with full context for review.
Hooks implement the separation of mechanism and policy. Claude's mechanism is powerful and general-purpose. Your hooks are where organizational policy lives — access rules, audit requirements, compliance checks. Neither should be tangled into the other. A well-designed hook system means you can change policy without modifying prompts, and upgrade Claude without rewriting your safety layer.
Hooks can be declared at two scopes. Project-level hooks live in .claude/settings.json within the repository and apply to all users working in that project. They are version-controlled alongside code and represent team-wide policy. User-level hooks live in ~/.claude/settings.json and apply to everything that user runs, regardless of project. User hooks are appropriate for personal audit logging, individual security preferences, or tooling that integrates with a developer's own environment (IDE plugins, local secret scanners).
When both scopes have hooks for the same event, both run. Project hooks fire first, then user hooks. If either blocks, the action is blocked.
Post-execution hooks are underused but powerful. After Claude writes a file, a PostToolUse hook can run a linter, a type-checker, a secret scanner (like trufflehog or gitleaks), or a custom validation script. If validation fails, the hook can write a corrective message back into Claude's context so the model can self-repair — turning what would have been a silent error into a feedback loop.
In 2024, the team at Temporal Technologies used PostToolUse hooks to run their internal API contract validator after every TypeScript file write. When Claude generated code that violated a gRPC contract, the hook output the validation error to stdout (which Claude Code surfaces as tool output), and Claude would immediately revise the file. Engineers reported that this pattern reduced code review cycles by catching a category of error that had previously only been caught at CI time.
You are a platform engineer at a fintech startup. Claude Code is being introduced for backend development. Your team handles PCI-compliant payment processing. You need to design a hook strategy that prevents Claude from accidentally touching production config files or writing code with hardcoded secrets.
In late 2023, Notion ran an internal pilot of an AI coding agent for their backend infrastructure team. One of their senior engineers, Akosua Mensah, documented the pilot's first major stumble in an internal postmortem that later became a public talk at a developer conference. The agent had been given access to a broad bash execution tool with no scope restrictions. Within a week, a routine "clean up unused imports" task led the agent — following a chain of perfectly reasonable intermediate steps — to run find / -name "*.pyc" -delete across the entire development environment, including a mounted network share that contained archived experiment logs used by the ML team.
No secrets were exposed. No production system was touched. But three weeks of experiment logs were gone, and the recovery process consumed two days of engineering time. Mensah's postmortem concluded with a single sentence that became something of a mantra for the team: "Give the agent a scalpel, not a chainsaw."
Claude Code's permission system operates at the tool level. You can explicitly allow or deny specific tools, and you can configure allowed path patterns for file operations. The system uses an allowlist and a denylist that work together: anything on the denylist is always blocked; anything not on the allowlist (in strict mode) is also blocked. This gives you two complementary control strategies.
The configuration key allowedTools takes an array of tool names or patterns. The key deniedTools takes the same structure but blocks regardless of other settings. You can also configure allowedDirectories to constrain file system access to specific path prefixes.
The Notion incident is a textbook illustration of why the principle of least privilege applies to AI agents as much as it does to human operators or service accounts. The agent should receive only the permissions necessary to accomplish the current task — no more. When the task changes, permissions should be re-evaluated.
In practice, this means designing different permission profiles for different task types. A documentation-writing session needs Read access to source files and Write access to a docs/ directory. It does not need Bash execution or access to environment variables. A refactoring session needs Read and Edit for source files. It does not need network access or the ability to modify CI configuration. You would not give a contractor building a deck access to every room in your house; the same logic applies here.
A locked-down refactoring profile in settings.json: "allowedTools": ["Read", "Edit", "Bash(grep *)", "Bash(find *)"] with "allowedDirectories": ["./src", "./tests"] — This gives Claude exactly what it needs for code navigation and editing within the source tree, with bash restricted to read-only search operations.
The Bash tool deserves special attention because it is both the most powerful and the most dangerous tool in Claude's default kit. Claude Code supports command-level scoping for Bash through the pattern syntax Bash(command pattern). This allows you to permit specific bash commands while denying others.
For example, Bash(npm test*) permits running test scripts but not arbitrary npm commands. Bash(git log*) permits reading git history but not commits or pushes. Bash(grep *) permits read-only text search. This granularity was not available in early versions of Claude Code and was specifically introduced in response to developer feedback about the risks of broad bash access — the exact category of risk that produced the Notion incident.
A common question: what happens if a tool appears in both allowedTools and deniedTools? The answer is unambiguous — deny always wins. This design choice is deliberate. It means that a cautious default can always be enforced by adding to the denylist, without having to audit or modify every allowlist across all contexts. Security teams can add a global user-level deny that overrides any project-level allow. This is the correct security posture: the most restrictive applicable rule governs.
Many teams adopt a task-scoped session model: before each major Claude Code session, a short wrapper script sets the appropriate settings.json for the task type (docs, refactor, test-writing, migration) and launches Claude with that profile. The session is scoped to the minimum necessary permissions. When the session ends, the default restrictive profile is restored. This is analogous to how database administrators use role-based access: you don't run every query as superuser.
Permission denials are not just safety events — they are signals. When Claude repeatedly attempts to use a denied tool, it often means the task scope is wrong (the model needs something you haven't provided), or the task description is ambiguous (Claude is interpreting the request differently than you intended), or — in adversarial scenarios — that a prompt injection has occurred and is attempting to break out of constraints. Logging denied tool attempts with full context (what tool, what arguments, what was in the conversation at that point) provides audit data that is genuinely useful for both debugging and security review.
You are setting up Claude Code for a team building a Node.js API. You need to create at least two permission profiles: one for writing unit tests (safe, limited) and one for database migration tasks (broader but still scoped). Each profile should specify allowedTools, deniedTools, and allowedDirectories appropriately.
In the spring of 2024, a development team using Cursor's agent mode to automate database schema migrations encountered a failure mode that became widely discussed in developer communities. The agent had been tasked with "migrate all remaining legacy tables to the new schema." It proceeded through seventeen tables successfully in a staging environment, then — without pausing — began the same process on a production database connection that had been left open in the same terminal session. The session context had not clearly delineated the environment boundary.
Four tables were migrated before a team member noticed the production database metrics changing. The migrations themselves were structurally correct, but they had not been reviewed for production data volume and locking behavior. The rollback took six hours. The team's incident review produced one core recommendation: for any action targeting a production resource, the agent must pause and explicitly confirm the target environment with a human before proceeding. Not a warning. Not a log entry. A pause that requires active acknowledgment.
Claude Code supports human-in-the-loop control through two mechanisms. The first is the --ask-permission flag (or equivalent settings configuration), which causes Claude to request user confirmation before executing any tool call. This is maximally safe but maximally interruptive — suitable for high-stakes or unfamiliar tasks, not for routine automation.
The second and more practical mechanism is hook-based confirmation gates: PreToolUse hooks that detect high-stakes conditions and pause for explicit confirmation rather than allowing or blocking outright. A hook can write a prompt to the terminal and wait for input before returning, effectively implementing a per-operation confirmation workflow that is triggered only when the conditions warrant it.
The key design challenge for confirmation gates is identifying which conditions should trigger them. The most useful heuristic is irreversibility: actions that cannot be easily undone deserve more caution than actions that can. Deleting a file without git tracking is irreversible. Modifying a production database record is hard to reverse. Running a test suite is freely reversible. This maps directly to how confirmation gates should be configured.
In practice, teams often define confirmation-triggering conditions along two axes: environment (is the target production, staging, or development?) and action type (is this destructive, modifying shared state, or accessing sensitive data?). The intersection of high-risk environment and destructive action is where confirmation gates earn their cost.
A PreToolUse hook for the Write tool that checks whether the target path contains "/prod/" or "/production/" in its name, then prompts: if [[ "$TARGET_PATH" == *"/prod/"* ]]; then read -p "Writing to production path $TARGET_PATH. Confirm? [y/N] " yn; [[ "$yn" == "y" ]] || exit 1; fi — This adds a single confirmation step only when the at-risk condition is present, leaving all other writes uninterrupted.
The better your task structure, the fewer interruptions you need. The Cursor incident happened in part because the task description did not make the environment boundary explicit. A well-scoped task would have said: "Migrate the following six tables in staging only. When complete, pause and show me the migration SQL for production review before any production execution." This is not just a safety practice — it produces better results, because Claude Code will write cleaner, more targeted migration code when the scope is precisely defined.
Teams at PlanetScale developed a pattern they called "dry run by default, wet run on demand" for AI-assisted schema migrations: every migration task was structured so that Claude's first output was always a dry run that showed exactly what would execute, requiring an explicit human instruction to proceed to actual execution. The dry run step was not a confirmation dialog — it was a designed phase in the task structure itself.
One often-overlooked aspect of human-in-the-loop design is what happens when the human doesn't respond. In long-running automation jobs, Claude Code may encounter a confirmation gate while the operator has stepped away. The safest default is fail closed: if no response is received within a timeout period, the hook exits with a non-zero code, blocking the action and leaving the state unchanged. This is preferable to proceeding on timeout, which turns a safety pause into just a delay.
Your confirmation gate hooks should include a timeout parameter and a clear log message explaining why execution was halted, so the operator returning to the terminal understands immediately what happened and can restart the task with appropriate context.
Human-in-the-loop is not a fallback for when automation fails — it is a deliberate architectural choice for operations where human judgment adds value that automation cannot substitute. The question to ask for each class of operation is: "Would a reasonable person expect to be informed before this happens?" If yes, build the gate. The cost of an interruption is almost always lower than the cost of an unwanted irreversible action.
You are building an automation pipeline where Claude Code helps with database migrations for a SaaS product with 50,000 users. You need to design confirmation gate hooks that distinguish between development, staging, and production targets, and that handle the timeout/fail-closed pattern correctly.
In April 2023, Samsung Semiconductor engineers made headlines for entirely the wrong reasons. Three separate incidents within a single month involved engineers pasting sensitive internal code into ChatGPT for assistance — including, in one case, source code containing database connection strings with production credentials, and in another, internal meeting notes about semiconductor yield problems. The information was sent to OpenAI's servers as part of the conversation context. Samsung subsequently banned the use of generative AI tools on company devices.
The incidents were not attacks. They were well-intentioned engineers doing their jobs — trying to get help with real problems. But they illustrate a failure mode that applies directly to Claude Code: when you give an AI model context, you are also giving it everything in that context. If your context includes credentials, those credentials have left the building.
Claude Code is a code agent that reads files, executes commands, and processes their output. This creates multiple vectors through which secrets can enter Claude's context. The most common are: reading a .env file to understand a project's configuration, executing a command that outputs a token or key in its response, reading a configuration file that embeds API keys, or processing error output that includes authentication details.
In most cases, Claude doesn't need the actual value of a secret — it needs to know that a secret exists and what its name is. The distinction matters enormously. Claude can write code that uses process.env.STRIPE_SECRET_KEY without ever knowing the value of that key. Giving Claude the actual key provides no benefit to the task and creates unnecessary exposure.
The most direct protection is to explicitly exclude secrets files from Claude's directory access. If your project uses a .env file at the root, and your allowedDirectories configuration includes ./src and ./tests but not the root directory or specific exclusions, Claude cannot read .env. This is often the correct configuration for development sessions focused on application code.
When Claude does need to read the project root — for configuration files, package.json, or similar — you can use a PreToolUse hook on the Read tool that blocks reads of specific file patterns. A hook that checks whether the target file matches *.env, *.pem, *secret*, *credential*, or *_key* in its filename and exits 1 on a match provides a targeted protection layer that doesn't require restructuring your directory permissions.
A PreToolUse hook on the Read tool: TARGET=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('file_path',''))") — then check the target against a pattern list. Matching patterns: .env, .env.*, *.pem, *.key, *secret*, *credential*, *.p12, *.pfx, id_rsa, id_ed25519. Exit 1 with a message if matched. This adds read protection without restructuring allowedDirectories.
A safer pattern for secrets that Claude's generated code will reference is environment variable injection at runtime rather than file-based configuration. If Claude is writing code that needs to call an API, it should write code that reads the key from an environment variable. When you test that code, you inject the variable into the process environment directly — the value never appears in a file Claude can read.
This is consistent with the twelve-factor app methodology and with security best practices generally: credentials belong in the environment, not in files. For Claude Code workflows, it also means that even if Claude has full read access to your source tree, it will encounter only variable names, not values. The actual credentials live in your secrets manager, your CI pipeline's secret store, or your shell's exported environment — none of which Claude reads by default.
Despite best efforts, secrets occasionally end up in code — hardcoded during a debug session, inserted by a model following an ambiguous example, or copy-pasted from a legacy file. This is why a PostToolUse secret scanning hook is a valuable second line of defense. Tools like trufflehog (open source, maintained by Truffle Security), gitleaks, and detect-secrets (from Yelp) can scan a file for credential patterns in under a second.
Configuring gitleaks as a PostToolUse hook on the Write and Edit tools means that every file Claude writes is immediately scanned. If gitleaks finds a match, the hook exits non-zero, Claude is notified that a secret may have been embedded, and Claude can revise the file before it is ever committed. This catches the category of problem that the Samsung engineers encountered before it has any chance of leaving the local environment.
No single control is sufficient. A complete secrets protection strategy for Claude Code combines: (1) allowedDirectories that exclude secrets files from Claude's reach; (2) PreToolUse hooks blocking reads of secrets-pattern filenames; (3) environment variable injection so code references names, not values; and (4) PostToolUse scanning for secrets in every file Claude writes. Each layer catches what slips through the others.
Even with all the above controls, maintaining an audit log of every file Claude reads and every command it executes provides a crucial recovery capability. If a secret is later found to have been exposed, the audit log tells you exactly when it happened, what Claude was doing, and what context it had. This supports incident response and also creates accountability that encourages disciplined configuration practices in the first place.
A simple audit log hook — a PreToolUse hook that appends a timestamped JSON record of every tool call to a rotating log file — costs almost nothing in performance and provides significant forensic value. Many teams maintain these logs alongside their CI artifacts for the duration of a project.
You are a security engineer at a startup using Claude Code for backend development. Your codebase has .env files, AWS credential files (~/.aws/credentials), and several config files with embedded API keys from before your secrets management migration. You need to design the full four-layer secrets protection model for your Claude Code setup.