HookStackGitHub
Back to catalogue
SecurityPreToolUse· Write|EditPreToolUseBefore tool execution · can block⚡ blocking

Write-time secret detection

No API key ever lands in a file

Scans the content being written (Write) or inserted (Edit new_string) for API keys, GitHub tokens, private keys and password assignments, and blocks the call before the secret ever lands on disk. Closes the file-write gap left by bash-level secret detection: an agent pasting a key into source code or a config file is stopped with a pointer to environment variables instead.

What does the Write-time secret detection hook do?

Write-time secret detection is a Claude Code PreToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. No API key ever lands in a file.

Use cases

  • Blocking an agent from hardcoding an API key into source code
  • Stopping .env values from being copied into committed files
  • Preventing private keys from being pasted into the repo

Tags

#security#secrets#prevention#write-guard

settings.json fragment

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/pre-write-secret-detection.mjs"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/pre-write-secret-detection.mjs

#!/usr/bin/env node
// Bloque les écritures de fichiers contenant des secrets potentiels (PreToolUse Write|Edit)
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';

const SECRET_PATTERNS = [
  /(?:ANTHROPIC|OPENAI|CLAUDE|GEMINI|GROQ)_API_KEY\s*=\s*['"]?\S{20,}/i,
  /sk-(?:ant-|proj-)?[a-zA-Z0-9_-]{32,}/,
  /ghp_[a-zA-Z0-9]{36}/,
  /-----BEGIN (?:RSA |EC )?PRIVATE KEY/,
  /(?:password|passwd|secret|token)\s*=\s*['"][^'"]{6,}/i,
];

export function run(input) {
  const content =
    input.tool_input?.content ?? input.tool_input?.new_string ?? '';
  if (!content) return null;

  const match = SECRET_PATTERNS.find((p) => p.test(content));
  if (!match) return null;

  return {
    decision: 'block',
    reason:
      '[secret-detection] Potential secret in the content being written. Reference it via an environment variable or .env (gitignored) instead of hardcoding it.',
  };
}

/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const input = JSON.parse(readFileSync(0, 'utf8'));
  const result = run(input);
  if (result) process.stdout.write(JSON.stringify(result));
}