feat: 4-layer secret leak prevention - #1811
Conversation
Implements https://zenn.dev/takna/articles/secret-leak-prevention-4-layer - Layer 1 (design): .gitignore patterns for credential dirs - Layer 2 (pre-commit): lefthook.yml + global gitleaks config at ~/.config/gitleaks/config.toml via home-manager - Layer 3: requires manual GitHub push-protection toggle (server-side) - Layer 4: shared secret-guard.sh PreToolUse hook for Claude Code and Codex Write/Edit/MultiEdit, with multi-shape payload extraction gitleaks and lefthook added to home-manager packages.
|
|
You do not have enough credits to review this pull request. Please purchase more credits to continue. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis pull request implements a comprehensive secret detection and prevention system that integrates Gitleaks scanning with Claude Code, Codex, and pre-commit workflows. It adds Gitleaks configuration with custom rules, a Bash hook script for pre-tool-use validation, and related tooling setup. ChangesSecret Detection and Prevention System
Sequence Diagram(s)sequenceDiagram
participant Tool as Claude Code/<br/>Codex
participant PreToolUse as PreToolUse<br/>Hook
participant SecretGuard as secret-guard.sh
participant Gitleaks as Gitleaks
participant User as User
Tool->>PreToolUse: Trigger Write/Edit/<br/>MultiEdit with payload
PreToolUse->>SecretGuard: Execute with 5s timeout
SecretGuard->>SecretGuard: Check jq & gitleaks<br/>available
SecretGuard->>SecretGuard: Extract content from<br/>JSON payload (jq)
alt Content found
SecretGuard->>SecretGuard: Write to temp directory
SecretGuard->>SecretGuard: Resolve config from<br/>.gitleaks.toml or env
SecretGuard->>Gitleaks: Run gitleaks dir<br/>--redact --verbose
alt Secrets detected
Gitleaks->>SecretGuard: Found secrets
SecretGuard->>User: Print error to stderr:<br/>Operation blocked
SecretGuard->>Tool: Exit 2 (block)
else No secrets
Gitleaks->>SecretGuard: No secrets found
SecretGuard->>Tool: Exit 0 (allow)
end
else No content
SecretGuard->>Tool: Exit 0 silently
end
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
1 issue found across 9 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="config/gitleaks/config.toml">
<violation number="1" location="config/gitleaks/config.toml:24">
P1: Avoid globally allowlisting the entire `dotagents/` directory; it disables secret scanning for all files there.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| '''(.*?)\.example$''', | ||
| '''\.env\.example$''', | ||
| '''docs/.*\.template\.md$''', | ||
| '''dotagents/.*''', |
There was a problem hiding this comment.
P1: Avoid globally allowlisting the entire dotagents/ directory; it disables secret scanning for all files there.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At config/gitleaks/config.toml, line 24:
<comment>Avoid globally allowlisting the entire `dotagents/` directory; it disables secret scanning for all files there.</comment>
<file context>
@@ -0,0 +1,37 @@
+ '''(.*?)\.example$''',
+ '''\.env\.example$''',
+ '''docs/.*\.template\.md$''',
+ '''dotagents/.*''',
+ '''bun\.lock$''',
+ '''Cargo\.lock$''',
</file context>
| .toolInput.content // | ||
| .toolInput.new_string // | ||
| (.tool_input.edits // [] | map(.new_string) | join("\n")) // | ||
| (.tool.input.edits // [] | map(.new_string) | join("\n")) // |
There was a problem hiding this comment.
Codex MultiEdit silently bypasses this hook.
jq's // only falls through on null/false. When .tool_input.edits is missing (the Codex case), .tool_input.edits // [] becomes [], and map(.new_string) | join("\n") turns that into "" — which is defined for //, so the chain short-circuits here and never evaluates .tool.input.edits.
Reproduced:
$ echo '{"tool":{"input":{"edits":[{"new_string":"AKIA1234567890ABCDEF"}]}}}' | jq -r '
.tool_input.content // .tool_input.new_string //
.tool.input.content // .tool.input.new_string //
.toolInput.content // .toolInput.new_string //
(.tool_input.edits // [] | map(.new_string) | join("\n")) //
(.tool.input.edits // [] | map(.new_string) | join("\n")) //
empty'
(empty)
Downstream the script hits [[ -z "$CONTENT" ]] && exit 0 and silently allows the write. Claude Write/Edit/MultiEdit and Codex Write/Edit still work — only Codex MultiEdit regresses.
Fix by coalescing the array sources before the join, e.g.:
(((.tool_input.edits // .tool.input.edits // .toolInput.edits) // [])
| map(.new_string) | join("\n")) //
empty| CONFIG_ARG=(--config "$HOME/.config/gitleaks/config.toml") | ||
| fi | ||
|
|
||
| if gitleaks dir "$TMP_DIR" "${CONFIG_ARG[@]}" --no-banner --redact >/dev/null 2>&1; then |
There was a problem hiding this comment.
Gitleaks errors are misreported as "secret found".
gitleaks returns 0 for clean, 1 for leaks, and other non-zero codes for errors (unknown subcommand on older builds, bad config, panic, etc.). if gitleaks … ; then exit 0; fi; exit 2 collapses all of those into the same generic "detected secrets" message, and 2>/dev/null hides the real reason — so a broken install or malformed GITLEAKS_CONFIG looks like every Write/Edit/MultiEdit suddenly contains a secret.
Consider capturing the exit code and falling open (or surfacing stderr) on anything that isn't 1, e.g.:
GITLEAKS_OUT=$(gitleaks dir "$TMP_DIR" "${CONFIG_ARG[@]}" --no-banner --redact 2>&1)
rc=$?
case "$rc" in
0) exit 0 ;;
1) ;; # fall through to block message
*) printf 'secret-guard: gitleaks errored (rc=%s); skipping\n%s\n' "$rc" "$GITLEAKS_OUT" >&2; exit 0 ;;
esacThat keeps the hook's self-stated "supplementary, not primary defense" posture (deps missing → silent exit 0).
| [[rules]] | ||
| id = "japanese-password-field" | ||
| description = "Detects Japanese password field patterns" | ||
| regex = '''(?i)(パスワード|password|pw|pass)\s*[::=]\s*["']?[A-Za-z0-9!@#$%^&*\-_=+]{6,}["']?''' |
There was a problem hiding this comment.
False-positive prone — no keywords, no word boundaries.
(?i)(パスワード|password|pw|pass)\s*[::=]\s*…{6,} matches pass: as a substring of bypass:, passphrase=, compass = …, etc. once the value has ≥6 chars from the allowed class. The API-key rule has the same shape.
Two small fixes that materially reduce noise without losing coverage:
[[rules]]
id = "japanese-password-field"
description = "Detects Japanese password field patterns"
keywords = ["password", "pw", "pass", "パスワード"]
regex = '''(?i)\b(パスワード|password|pw|pass)\b\s*[::=]\s*["']?[A-Za-z0-9!@#$%^&*\-_=+]{6,}["']?'''keywords also lets gitleaks prefilter so the regex doesn't run against every line — useful for the lefthook pre-commit path on large diffs.
| *.local.md | ||
| *.secret.md | ||
| credentials*.md | ||
| **/draft/ |
There was a problem hiding this comment.
**/draft/ matches any draft/ directory at any depth, not just under credential paths. In a dotfiles tree that nests .worktrees/, dotagents/, and other repos, this can silently swallow legitimate draft/ content in unrelated projects.
The other patterns in this block (_credentials/, *.local.md, *.secret.md, credentials*.md) are intentionally narrow — consider anchoring this one too, e.g. /_credentials/draft/, or switching to a filename pattern like *.draft.md to match the existing style.
There was a problem hiding this comment.
Code Review
This pull request implements a multi-layered secret leak prevention system by integrating Gitleaks into AI tool hooks (Claude and Codex) and git pre-commit hooks via Lefthook. It includes custom Gitleaks rules for Japanese patterns and updates .gitignore to exclude credential locations. Feedback focuses on ensuring compatibility with Gitleaks v8 by updating deprecated commands (dir to detect --source and git to protect) and fixing a logic bug in the jq filter used to extract content from tool payloads.
| CONFIG_ARG=(--config "$HOME/.config/gitleaks/config.toml") | ||
| fi | ||
|
|
||
| if gitleaks dir "$TMP_DIR" "${CONFIG_ARG[@]}" --no-banner --redact >/dev/null 2>&1; then |
There was a problem hiding this comment.
Gitleaks v8 (which is standard in Nixpkgs) has replaced the dir command with detect --source. Using the old dir syntax will cause the command to fail with an error. Since stderr is redirected to /dev/null and the script exits with a non-zero code on failure, this will result in blocking ALL AI writes regardless of whether they contain secrets.
| if gitleaks dir "$TMP_DIR" "${CONFIG_ARG[@]}" --no-banner --redact >/dev/null 2>&1; then | |
| if gitleaks detect --source "$TMP_DIR" "${CONFIG_ARG[@]}" --no-banner --redact >/dev/null 2>&1; then |
| PAYLOAD=$(cat) | ||
|
|
||
| # Extract content across Claude and Codex payload shapes (Write/Edit/MultiEdit). | ||
| CONTENT=$(printf '%s' "$PAYLOAD" | jq -r ' | ||
| .tool_input.content // | ||
| .tool_input.new_string // | ||
| .tool.input.content // | ||
| .tool.input.new_string // | ||
| .toolInput.content // | ||
| .toolInput.new_string // | ||
| (.tool_input.edits // [] | map(.new_string) | join("\n")) // | ||
| (.tool.input.edits // [] | map(.new_string) | join("\n")) // | ||
| empty | ||
| ' 2>/dev/null) |
There was a problem hiding this comment.
The jq filter has a logic bug where "" (produced by join("\n") on an empty or missing edits array) is considered truthy in jq. This causes the // chain to stop prematurely, potentially skipping subsequent checks (e.g., Codex fields might be skipped if Claude edits is missing). Additionally, reading the entire payload into a shell variable is inefficient for large tool inputs. Piping stdin directly to jq is more robust.
| PAYLOAD=$(cat) | |
| # Extract content across Claude and Codex payload shapes (Write/Edit/MultiEdit). | |
| CONTENT=$(printf '%s' "$PAYLOAD" | jq -r ' | |
| .tool_input.content // | |
| .tool_input.new_string // | |
| .tool.input.content // | |
| .tool.input.new_string // | |
| .toolInput.content // | |
| .toolInput.new_string // | |
| (.tool_input.edits // [] | map(.new_string) | join("\n")) // | |
| (.tool.input.edits // [] | map(.new_string) | join("\n")) // | |
| empty | |
| ' 2>/dev/null) | |
| # Extract content across Claude and Codex payload shapes (Write/Edit/MultiEdit). | |
| CONTENT=$(jq -r ' | |
| [ | |
| .tool_input.content, | |
| .tool_input.new_string, | |
| .tool.input.content, | |
| .tool.input.new_string, | |
| .toolInput.content, | |
| .toolInput.new_string, | |
| (.tool_input.edits | select(.) | map(.new_string) | join("\n")), | |
| (.tool.input.edits | select(.) | map(.new_string) | join("\n")) | |
| ] | map(select(. != null and . != "")) | .[0] // empty | |
| ' 2>/dev/null) |
| parallel: true | ||
| commands: | ||
| gitleaks: | ||
| run: gitleaks git --staged --redact --verbose |
Summary
Implements the 4-layer secret leak prevention pattern from https://zenn.dev/takna/articles/secret-leak-prevention-4-layer.
.gitignorepatterns for credential locations (_credentials/,*.local.md,*.secret.md,credentials*.md,**/draft/).lefthook.ymlrunsgitleaks git --staged --redact --verbose. Gitleaks config moved toconfig/gitleaks/config.tomland installed globally at~/.config/gitleaks/config.tomlvia home-manager;GITLEAKS_CONFIGenv var is set so every repo picks up the same baseline without a per-repo.gitleaks.toml.config/shared/hooks/secret-guard.shis a shared PreToolUse hook wired into both Claude Code (config/claude/settings.json) and Codex (config/codex/hooks.json) forWrite|Edit|MultiEdit. It extracts content from multiple payload shapes (Claudetool_input.*, Codextool.input.*, plus MultiEditedits[].new_string), runs gitleaks against the extracted content, and exits 2 to block on detection. Skips silently ifgitleaks/jqare missing so it does not break before the next rebuild.gitleaksandlefthookadded tohome-manager/packages/default.nix.Test plan
darwin-rebuild switch --flake .installsgitleaksandlefthooklefthook installwires.git/hooks/pre-commitin this repoecho 'AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE' | gitleaks stdinflagsWritewith a fake AWS key in content is blocked bysecret-guard.shWritewith the same payload is blockedSummary by cubic
Adds a 4-layer secret leak prevention system across ignore rules, pre-commit checks, server push protection, and editor hooks. Also adds shell coverage for
secret-guard.shand formats the script.New Features
.gitignoreexcludes_credentials/,*.local.md,*.secret.md,credentials*.md,**/draft/.lefthookrunsgitleaks git --staged --redact --verbose; global config at~/.config/gitleaks/config.tomlviahome-managerwith custom rules and allowlist.secret-guard.shhooks Claude Code and Codex Write/Edit/MultiEdit, scans payloads withgitleaks, exits 2 on findings; skips ifgitleaks/jqmissing.Dependencies
gitleaksandlefthooktohome-managerpackages.Written for commit 6ffefd1. Summary will update on new commits. Review in cubic