How the PreToolUse Hook Guardrail Detects Hardcoded Secrets in Claude Code Harness

The PreToolUse hook guardrail intercepts tool operations before execution by scanning code diffs with regex pattern R09 to identify hardcoded API keys, tokens, and passwords, immediately denying any operation that would expose secrets to the repository.

The claude-code-harness repository implements a security-focused PreToolUse guardrail that prevents hardcoded secrets from entering your codebase. This Go-based hook analyzes file changes in real-time using declarative regex rules to catch credential assignments before they reach version control. The system evaluates every file modification through rule R09:warn-secret-file-read, which specifically targets secret-like variable assignments in the changed lines.

How the Detection Pipeline Works

The guardrail engine processes potential secrets through a multi-stage pipeline that executes before any tool can modify the repository.

Building the Hook Input Context

When a tool like Write, Edit, or Bash is invoked, the runtime constructs a hookproto.HookInput structure containing the command details, target file paths, and the diff of proposed changes. This input is passed to the EvaluatePreTool function in go/internal/guardrail/pre_tool.go (lines 180-210), which serves as the entry point for all PreToolUse validations.

Evaluating Declarative Guardrail Rules

The EvaluatePreTool function forwards the input to the generic guardrail engine via guardrail.Evaluate. This engine iterates over the declarative rule list defined in go/internal/guardrail/rules.go. The specific rule for secret detection is R09:warn-secret-file-read, which contains a case-insensitive regex pattern that matches assignments such as API_KEY = "abcd1234…", secret = "s3cr3t!", or token="...".

When the regex matches any line in the diff, the rule returns a hookproto.HookResult with DecisionDeny and a human-readable reason explaining the detection.

Converting Results to Hook Responses

The PreToolToOutput function (lines 195-214 in go/internal/guardrail/pre_tool.go) converts the HookResult into an official JSON payload conforming to the PreToolOutput structure defined in go/pkg/hookproto/types.go. The resulting JSON includes "permissionDecision":"deny" and "permissionDecisionReason":"hardcoded secret …".

Hook Wiring and Execution

The PreToolUse hook is declared in hooks/hooks.json with a matcher covering "Write|Edit|MultiEdit|Bash|Read" operations. When triggered, it executes scripts/pretooluse-guard.sh, which reads the JSON response and aborts the tool execution with exit code 2 if a denial decision is returned. This prevents the secret from ever being committed to the repository.

The Secret Detection Regex Pattern

The core detection logic resides in the regex definition within rule R09 in go/internal/guardrail/rules.go:

{
    ID:          "R09:warn-secret-file-read",
    Description: "Detect hard‑coded secret‑like assignments",
    // Matches: api_key = "....", secret: '....', token = "....", etc.
    Regex:       `(?i)\b(api[_-]?key|secret|token|password|passwd|client[_-]?secret)\b[^:=\n]{0,20}[:=][[:space:]]*['"][^'"]{8,}['"]`,
    Decision:    DecisionDeny,
}

This pattern is case-insensitive and looks for common secret identifiers including api_key, secret, token, password, passwd, and client_secret. The regex allows up to 20 characters between the identifier and the assignment operator (= or :), and requires a quoted value of at least 8 characters. This minimum length requirement filters out trivial false positives while catching real credentials.

Code Examples

Blocked Change with Hardcoded API Key

Consider a modification to src/config.js that attempts to hardcode sensitive credentials:

--- a/src/config.js
+++ b/src/config.js
@@
-// TODO: inject from environment
-const API_KEY = "abcd1234efgh5678ijkl9012mnop3456";
+const API_KEY = process.env.API_KEY;   // ✅ fixed

When running claude-code write src/config.js, the PreToolUse guardrail triggers and returns a denial response:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "hardcoded secret assignment (API_KEY)",
    "additionalContext": null
  }
}

The pretooluse-guard.sh script receives this JSON and exits with code 2, aborting the write operation before the secret enters the repository.

Allowed Change Without Secrets

A modification containing no secret patterns proceeds normally:

--- a/src/util.js
+++ b/src/util.js
@@
-const DEBUG = true;
+const DEBUG = false;

The guardrail returns an allowance response:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow"
  }
}

The tool execution continues without interruption.

Summary

  • The PreToolUse guardrail operates before any tool execution, intercepting Write, Edit, Bash, Read, and MultiEdit operations as configured in hooks/hooks.json.
  • Detection relies on rule R09:warn-secret-file-read in go/internal/guardrail/rules.go, which uses a case-insensitive regex to identify hardcoded credentials.
  • The system analyzes only the actual diff of changes rather than scanning the entire repository, ensuring fast performance with minimal false positives.
  • Upon detection, the guardrail returns a JSON response with permissionDecision: deny, causing scripts/pretooluse-guard.sh to exit with code 2 and abort the operation.
  • All secret detection logic is declarative and centralized in Go structs, making pattern updates straightforward without modifying the underlying hook infrastructure.

Frequently Asked Questions

What tools does the PreToolUse guardrail monitor?

The guardrail monitors all file-modifying tools including Write, Edit, MultiEdit, Bash, and Read operations. This coverage is defined in the matcher field of hooks/hooks.json, ensuring comprehensive protection across every interface that could introduce hardcoded secrets into the repository.

How does the regex avoid false positives on short strings?

The regex pattern in rule R09 requires a minimum quoted value length of 8 characters ([^'"]{8,}) and restricts the match to specific secret-related keywords like api_key, secret, and token. This design filters out common variable names like secret = true or short placeholders while catching actual credential material.

Can I customize the secret detection patterns?

Yes, because the guardrail uses a declarative rule system defined in go/internal/guardrail/rules.go. You can modify the regex field in the R09 rule definition or add additional rules with different pattern IDs to extend detection for specific credential types used in your organization.

What happens when a secret is detected?

When the EvaluatePreTool function identifies a hardcoded secret, it returns a DecisionDeny result that PreToolToOutput converts to a JSON response with "permissionDecision":"deny". The pretooluse-guard.sh script interprets this response and exits with code 2, causing the Claude runtime to abort the tool operation immediately before any file changes are persisted.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →