Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Two-tier secret-leak guard, run on every local commit.
# Tier 1: auto-redact gitleaks findings inside src/_generated_ (driven live
# by Rule/gitleaks.toml via scripts/patch-generated-secrets.js, not
# a hand-maintained list) and re-stage the files it touched. Never
# blocks the commit by itself - it either fixes generated code or
# leaves it untouched for tier 2 to catch.
# Tier 2: run the real gitleaks scan against the staged diff and block on
# any finding tier 1 didn't (or couldn't) resolve.
# Installed via the repo's own "prepare" npm script - see package.json.
set -uo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

generated_dir="src/ _generated_"

# Snapshot what's already staged in the generated dir before tier 1 runs, so
# an intentionally-unstaged, in-progress change there (e.g. mid-way through
# testing a Fern regen) isn't unconditionally swept into this commit. Tier 1
# only edits working-tree files, never the index, so this snapshot stays
# accurate regardless of what it does next.
already_staged_in_generated="$(git diff --cached --name-only -- "$generated_dir" || true)"

git_dir="$(git rev-parse --git-dir)"
touched_file_list="$git_dir/leak-guard-touched-files.txt"
# A list from a previous commit's tier 1 run must never be reused here - if
# tier 1 doesn't run this time (node missing, below), stale entries from
# that earlier run would otherwise get staged again.
rm -f "$touched_file_list"

# git invokes hooks with a leaner PATH than your interactive shell, so a
# Node install managed by nvm/volta/fnm or bundled with an IDE (rather than
# a system package) is often invisible here even though `node` works fine
# in your terminal. If that's you, run this once and commit again:
# git config leakguard.nodepath "$(dirname "$(command -v node)")"
# It's a local git config value (not committed), so it won't affect anyone
# else's machine.
if ! command -v node >/dev/null 2>&1; then
custom_node_dir="$(git config --get leakguard.nodepath || true)"
[ -n "$custom_node_dir" ] && PATH="$custom_node_dir:$PATH" && export PATH
fi

if ! command -v node >/dev/null 2>&1; then
echo "[leak-guard] tier 1 skipped: 'node' not found on PATH inside the git hook environment."
echo "[leak-guard] If 'node' works in your terminal, git hooks are likely just seeing a different PATH."
echo "[leak-guard] Fix: git config leakguard.nodepath \"\$(dirname \"\$(command -v node)\")\", then commit again."
echo "[leak-guard] continuing to tier 2; this alone will not block the commit."
else
echo "[leak-guard] tier 1: auto-redacting gitleaks findings in generated code..."
if ! node scripts/patch-generated-secrets.js; then
echo "[leak-guard] tier 1 couldn't fully resolve generated code - see the message above."
echo "[leak-guard] continuing to tier 2; this alone will not block the commit."
fi
fi

# Stages only what tier 1 actually touched this run (recorded to
# $touched_file_list - see recordTouchedFiles in the script) plus whatever
# was already staged above. A file sitting in src/_generated_ that tier 1
# didn't touch and the caller hadn't staged is left alone rather than
# force-added - see the med-severity review finding on the old
# unconditional `git add` of the whole directory.
to_stage=()
if [ -f "$touched_file_list" ]; then
while IFS= read -r f; do
[ -n "$f" ] && to_stage+=("$f")
done < "$touched_file_list"
fi
while IFS= read -r f; do
[ -n "$f" ] && to_stage+=("$f")
done <<< "$already_staged_in_generated"

if [ "${#to_stage[@]}" -gt 0 ]; then
git add -- "${to_stage[@]}"
fi

if ! command -v gitleaks >/dev/null 2>&1; then
echo "[leak-guard] tier 2 skipped: 'gitleaks' binary not found locally."
echo "[leak-guard] CI will still scan this PR - install gitleaks locally to catch issues before pushing."
exit 0
Comment on lines +77 to +80
fi

echo "[leak-guard] tier 2: scanning staged changes with gitleaks..."
if ! gitleaks protect --staged --config="Rule/gitleaks.toml" --redact; then
echo ""
echo "[leak-guard] commit blocked - gitleaks found something in your staged changes."
echo "[leak-guard] known false positive in generated code -> re-run tier 1 (node scripts/patch-generated-secrets.js) and check its output."
echo "[leak-guard] false positive elsewhere -> add an allowlist entry to Rule/gitleaks.toml."
echo "[leak-guard] real secret -> remove it and rotate the credential before committing."
exit 1
fi

echo "[leak-guard] clean - proceeding with commit."
exit 0
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"lint": "npm run eslint",
"lint-fix": "prettier --write '**/*.{js,ts}' && eslint --fix '**/*.{js,ts}'",
"spellcheck": "cspell '**/*.{ts,js,md}'",
"docs-gen": "typedoc && node scripts/docs-script/markdown-gen.js && npx ts-node scripts/docs-script/processMarkdown.ts"
"docs-gen": "typedoc && node scripts/docs-script/markdown-gen.js && npx ts-node scripts/docs-script/processMarkdown.ts",
"patch-generated-secrets": "node scripts/patch-generated-secrets.js",
"prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath .githooks || true"
},
"repository": {
"type": "git",
Expand Down
Loading
Loading