Skip to content

feat: 4-layer secret leak prevention - #1811

Merged
shunkakinoki merged 3 commits into
mainfrom
feat/secret-leak-prevention-4-layer
May 19, 2026
Merged

feat: 4-layer secret leak prevention#1811
shunkakinoki merged 3 commits into
mainfrom
feat/secret-leak-prevention-4-layer

Conversation

@shunkakinoki

@shunkakinoki shunkakinoki commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the 4-layer secret leak prevention pattern from https://zenn.dev/takna/articles/secret-leak-prevention-4-layer.

  • Layer 1 (design): .gitignore patterns for credential locations (_credentials/, *.local.md, *.secret.md, credentials*.md, **/draft/).
  • Layer 2 (pre-commit): lefthook.yml runs gitleaks git --staged --redact --verbose. Gitleaks config moved to config/gitleaks/config.toml and installed globally at ~/.config/gitleaks/config.toml via home-manager; GITLEAKS_CONFIG env var is set so every repo picks up the same baseline without a per-repo .gitleaks.toml.
  • Layer 3 (push protection): server-side, enable manually per repo at Settings -> Advanced Security -> Secret Protection -> Push protection.
  • Layer 4 (AI hook): config/shared/hooks/secret-guard.sh is a shared PreToolUse hook wired into both Claude Code (config/claude/settings.json) and Codex (config/codex/hooks.json) for Write|Edit|MultiEdit. It extracts content from multiple payload shapes (Claude tool_input.*, Codex tool.input.*, plus MultiEdit edits[].new_string), runs gitleaks against the extracted content, and exits 2 to block on detection. Skips silently if gitleaks/jq are missing so it does not break before the next rebuild.

gitleaks and lefthook added to home-manager/packages/default.nix.

Test plan

  • darwin-rebuild switch --flake . installs gitleaks and lefthook
  • lefthook install wires .git/hooks/pre-commit in this repo
  • echo 'AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE' | gitleaks stdin flags
  • Claude Code Write with a fake AWS key in content is blocked by secret-guard.sh
  • Codex Write with the same payload is blocked
  • Clean writes pass through (exit 0)

Summary 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.sh and formats the script.

  • New Features

    • Layer 1: .gitignore excludes _credentials/, *.local.md, *.secret.md, credentials*.md, **/draft/.
    • Layer 2: Pre-commit via lefthook runs gitleaks git --staged --redact --verbose; global config at ~/.config/gitleaks/config.toml via home-manager with custom rules and allowlist.
    • Layer 3: Push protection (server-side). Enable per repo in Settings > Advanced Security > Secret Protection.
    • Layer 4: secret-guard.sh hooks Claude Code and Codex Write/Edit/MultiEdit, scans payloads with gitleaks, exits 2 on findings; skips if gitleaks/jq missing.
  • Dependencies

    • Added gitleaks and lefthook to home-manager packages.

Written for commit 6ffefd1. Summary will update on new commits. Review in cubic

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.
@indent-zero

indent-zero Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Adds a 4-layer secret-leak prevention setup (per the linked zenn.dev article): structural .gitignore patterns for credential paths, a global gitleaks config installed via a new home-manager module, a lefthook.yml pre-commit hook running gitleaks git --staged, and a shared secret-guard.sh PreToolUse hook wired into both Claude Code and Codex on Write|Edit|MultiEdit. gitleaks and lefthook are added to the home-manager package set.

  • Add _credentials/, *.local.md, *.secret.md, credentials*.md, **/draft/ to .gitignore (Layer 1).
  • Add config/gitleaks/{config.toml,default.nix} with English + Japanese password/API-key rules, an allowlist for templates/dummies, and GITLEAKS_CONFIG env wiring; register the module in config/default.nix.
  • Add lefthook.yml running gitleaks git --staged --redact --verbose on pre-commit (Layer 2).
  • Add config/shared/hooks/secret-guard.sh that extracts content from Claude/Codex Write/Edit/MultiEdit payloads via jq, writes it to a temp file, and runs gitleaks dir to block on detection with exit 2 (Layer 4).
  • Wire the new hook into config/claude/settings.json and config/codex/hooks.json PreToolUse Write|Edit|MultiEdit matchers.
  • Add gitleaks and lefthook to home-manager/packages/default.nix.

Issues

4 potential issues found:

  • .gitignore adds **/draft/, which matches any draft/ directory at any depth — including unrelated nested repos/worktrees under the dotfiles tree — and will silently stop tracking files in those legitimate draft/ dirs. → Autofix
  • The custom japanese-password-field / japanese-api-key-field rules omit keywords and the alternation has no word boundaries, so pass matches inside bypass:, passphrase=, etc., producing false-positive blocks on ordinary code. → Autofix
  • Codex MultiEdit silently bypasses secret-guard: the jq filter's first edits alternative always returns "" (truthy under jq's //), so the Codex .tool.input.edits branch is unreachable and Codex MultiEdit writes with secrets are accepted without scanning. → Autofix
  • secret-guard.sh treats every non-zero gitleaks exit as "secret found"; a broken/old gitleaks binary or malformed GITLEAKS_CONFIG will block every Write/Edit/MultiEdit with the generic "detected secrets" message and no diagnostic, since stderr is discarded. → Autofix

CI Checks

The shell-test failure was addressed in 6ffefd15 by adding config/shared/hooks/secret-guard.sh to spec/coverage_spec.sh's covered_scripts list; shell-check was downstream of that and will go green once shell-test reruns. Mesa Description remains neutral (not a failure). Waiting on the next CI run to confirm.


⚡ Autofix All Issues

@mesa-dot-dev

mesa-dot-dev Bot commented May 18, 2026

Copy link
Copy Markdown

You do not have enough credits to review this pull request. Please purchase more credits to continue.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63b576ca-7427-4c2f-9981-a2c5a3f418c2

📥 Commits

Reviewing files that changed from the base of the PR and between 68ca75d and 6ffefd1.

📒 Files selected for processing (10)
  • .gitignore
  • config/claude/settings.json
  • config/codex/hooks.json
  • config/default.nix
  • config/gitleaks/config.toml
  • config/gitleaks/default.nix
  • config/shared/hooks/secret-guard.sh
  • home-manager/packages/default.nix
  • lefthook.yml
  • spec/coverage_spec.sh

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Secret detection integrated into code operations to prevent credential leaks before they occur.
    • Pre-commit scanning that detects and blocks secrets before code is committed.
    • Automatic allowlisting of common dummy placeholders and example patterns to reduce false positives.
  • Chores

    • Added security scanning tools to project dependencies.
    • Updated gitignore to exclude credential-related paths and files.

Walkthrough

This 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.

Changes

Secret Detection and Prevention System

Layer / File(s) Summary
Gitleaks Detection Rules and Allowlist
config/gitleaks/config.toml
Gitleaks configuration with default rules extended by custom regex patterns for Japanese password and API key detection; allowlist suppresses false positives from lockfiles, node_modules, templates, and dummy placeholder values.
Gitleaks Installation and Environment Setup
config/gitleaks/default.nix, home-manager/packages/default.nix, config/default.nix
Gitleaks and Lefthook packages are added to the package list; a Nix module installs the Gitleaks config to ~/.config/gitleaks/config.toml and sets GITLEAKS_CONFIG environment variable; the module is registered in the main configuration.
Secret-Guard Pre-Tool-Use Hook
config/shared/hooks/secret-guard.sh, spec/coverage_spec.sh
New Bash hook script validates jq and gitleaks presence, extracts content from Claude/Codex JSON payloads across multiple schema shapes, resolves Gitleaks config from project or home directory, runs detection with redaction, blocks operations (exit 2) when secrets found, and cleans up temporary files; test coverage spec is updated to include the new script.
Claude/Codex PreToolUse Hook Integration
config/claude/settings.json, config/codex/hooks.json
Secret-guard hook is wired as a PreToolUse check for Write|Edit|MultiEdit operations in both Claude Code and Codex configurations with a 5-second timeout.
Pre-Commit and Gitignore Safeguards
lefthook.yml, .gitignore
Lefthook pre-commit configuration scans staged changes with Gitleaks in redaction and verbose modes; .gitignore adds a "Secrets - Layer 1 (design)" section excluding credential directories, secret/local markdown files, and draft directories.

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
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • shunkakinoki/dotfiles#1262: Modifies Claude Code PreToolUse hook settings for Write|Edit|MultiEdit matcher alongside the secret-guard hook integration.
  • shunkakinoki/dotfiles#1367: Expands .gitignore ignore patterns for AI/tooling artifacts in parallel with the new credential exclusion section.

Poem

🐰 A guard at the gate with secrets to keep,
Gitleaks scanning before the code runs deep,
Japanese patterns now caught in the night,
Pre-commit checks keep the dotfiles tight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: 4-layer secret leak prevention' clearly and concisely summarizes the main change—a comprehensive 4-layer secret leak prevention system implemented across multiple configuration layers.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing all four layers of the secret leak prevention pattern, the implementation approach, dependencies added, and a test plan.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/secret-leak-prevention-4-layer

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/.*''',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")) //

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ;;
esac

That 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,}["']?'''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .gitignore
*.local.md
*.secret.md
credentials*.md
**/draft/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

**/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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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

Comment on lines +11 to +24
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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)

Comment thread lefthook.yml
parallel: true
commands:
gitleaks:
run: gitleaks git --staged --redact --verbose

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Gitleaks v8 has replaced the git command with protect. The git --staged syntax is deprecated and may not work with the version installed via Nixpkgs.

      run: gitleaks protect --staged --redact --verbose

@shunkakinoki
shunkakinoki merged commit 7f49672 into main May 19, 2026
39 checks passed
@shunkakinoki
shunkakinoki deleted the feat/secret-leak-prevention-4-layer branch May 19, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant