diff --git a/.gitignore b/.gitignore index 7628531f3..3876246b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# Secrets - Layer 1 (design): structurally exclude credential locations +_credentials/ +*.local.md +*.secret.md +credentials*.md +**/draft/ + # AI .aider.tags.cache.v4 .claude diff --git a/config/claude/settings.json b/config/claude/settings.json index 93616c2e5..6ee6aeab3 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -257,6 +257,11 @@ { "matcher": "Write|Edit|MultiEdit", "hooks": [ + { + "type": "command", + "command": "$HOME/dotfiles/config/shared/hooks/secret-guard.sh", + "timeout": 5 + }, { "type": "command", "command": "git-ai checkpoint claude --hook-input stdin", diff --git a/config/codex/hooks.json b/config/codex/hooks.json index a08e38bbd..8f1d2fdb1 100644 --- a/config/codex/hooks.json +++ b/config/codex/hooks.json @@ -20,6 +20,16 @@ } ], "PreToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$HOME/dotfiles/config/shared/hooks/secret-guard.sh", + "timeout": 5 + } + ] + }, { "matcher": "Bash", "hooks": [ diff --git a/config/default.nix b/config/default.nix index ad41ffe7c..6e94844ca 100644 --- a/config/default.nix +++ b/config/default.nix @@ -18,6 +18,7 @@ in ./factory ./gemini ./git-ai + ./gitleaks ./gomi ./ghostty ./hermes diff --git a/config/gitleaks/config.toml b/config/gitleaks/config.toml new file mode 100644 index 000000000..3338a6453 --- /dev/null +++ b/config/gitleaks/config.toml @@ -0,0 +1,37 @@ +title = "dotfiles gitleaks config" + +# Extend the default gitleaks rules +[extend] +useDefault = true + +[[rules]] +id = "japanese-password-field" +description = "Detects Japanese password field patterns" +regex = '''(?i)(パスワード|password|pw|pass)\s*[::=]\s*["']?[A-Za-z0-9!@#$%^&*\-_=+]{6,}["']?''' + +[[rules]] +id = "japanese-api-key-field" +description = "Detects Japanese API key field patterns" +regex = '''(?i)(APIキー|api[-_ ]?key|secret[-_ ]?key|access[-_ ]?token)\s*[::=]\s*["']?[A-Za-z0-9!@#$%^&*\-_=+]{16,}["']?''' + +[allowlist] +description = "Global allowlist for templates, examples, and known-safe dummies" +paths = [ + '''(.*?)\.template\.md$''', + '''(.*?)\.example$''', + '''\.env\.example$''', + '''docs/.*\.template\.md$''', + '''dotagents/.*''', + '''bun\.lock$''', + '''Cargo\.lock$''', + '''flake\.lock$''', + '''node_modules/.*''', + '''target/.*''', +] +regexes = [ + '''DUMMY_PASSWORD''', + '''''', + '''''', + '''xxxxxxxxxxxxxxxx''', + '''example\.com''', +] diff --git a/config/gitleaks/default.nix b/config/gitleaks/default.nix new file mode 100644 index 000000000..1a9769481 --- /dev/null +++ b/config/gitleaks/default.nix @@ -0,0 +1,8 @@ +{ config, ... }: +{ + # Gitleaks reads $GITLEAKS_CONFIG when run outside a repo with a local .gitleaks.toml. + home.file.".config/gitleaks/config.toml".source = ./config.toml; + home.sessionVariables = { + GITLEAKS_CONFIG = "${config.home.homeDirectory}/.config/gitleaks/config.toml"; + }; +} diff --git a/config/shared/hooks/secret-guard.sh b/config/shared/hooks/secret-guard.sh new file mode 100755 index 000000000..10e954cd9 --- /dev/null +++ b/config/shared/hooks/secret-guard.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Shared PreToolUse hook for Claude Code and Codex Write/Edit operations. +# Blocks writes containing secrets detected by gitleaks. +# See: https://zenn.dev/takna/articles/secret-leak-prevention-4-layer +set -euo pipefail + +# Hook is supplementary, not primary defense. Skip silently if deps missing. +command -v jq >/dev/null 2>&1 || exit 0 +command -v gitleaks >/dev/null 2>&1 || exit 0 + +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) + +[[ -z $CONTENT || $CONTENT == "null" ]] && exit 0 + +TMP_DIR=$(mktemp -d -t secret-guard.XXXXXX) +trap 'rm -rf "$TMP_DIR"' EXIT +printf '%s' "$CONTENT" >"$TMP_DIR/payload.txt" + +CONFIG_ARG=() +if [[ -f "$PWD/.gitleaks.toml" ]]; then + CONFIG_ARG=(--config "$PWD/.gitleaks.toml") +elif [[ -n ${GITLEAKS_CONFIG:-} && -f $GITLEAKS_CONFIG ]]; then + CONFIG_ARG=(--config "$GITLEAKS_CONFIG") +elif [[ -f "$HOME/.config/gitleaks/config.toml" ]]; then + CONFIG_ARG=(--config "$HOME/.config/gitleaks/config.toml") +fi + +if gitleaks dir "$TMP_DIR" "${CONFIG_ARG[@]}" --no-banner --redact >/dev/null 2>&1; then + exit 0 +fi + +cat >&2 <<'EOF' +secret-guard: Blocked Write/Edit due to detected secrets. +- Move credentials to .gitignore'd paths (_credentials/, *.secret.md, etc.) +- Use explicit placeholders like or DUMMY_PASSWORD +- See: https://zenn.dev/takna/articles/secret-leak-prevention-4-layer +EOF +exit 2 diff --git a/home-manager/packages/default.nix b/home-manager/packages/default.nix index ee9f61197..d760358fc 100644 --- a/home-manager/packages/default.nix +++ b/home-manager/packages/default.nix @@ -49,6 +49,7 @@ with pkgs; fzf-make gh git + gitleaks glance glow gnumake @@ -66,6 +67,7 @@ with pkgs; just k6 lean4 + lefthook (if stdenv.isLinux && isDesktop then llama-cpp.override { vulkanSupport = true; } else llama-cpp) llm lsof diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 000000000..90af1e0c7 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,5 @@ +pre-commit: + parallel: true + commands: + gitleaks: + run: gitleaks git --staged --redact --verbose diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 5bf0ce674..400d74727 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -382,6 +382,7 @@ config/copilot/hooks/rtk-rewrite.sh config/copilot/hooks/security.sh config/shared/hooks/block-gh-settings.sh config/shared/hooks/block-git-push.sh +config/shared/hooks/secret-guard.sh config/cursor/activate.sh config/gemini/activate.sh config/git-ai/activate.sh