Skip to content

Commit a7930be

Browse files
committed
feat(shell): add bash pre-commit secret redaction guard
1 parent 23774b9 commit a7930be

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

.githooks/pre-commit

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
node scripts/split-knowledge-large-files.js
4+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
5+
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
6+
cd "$REPO_ROOT"
57

8+
node scripts/split-knowledge-large-files.js
69
if [ -d ".knowledge" ]; then
710
git add -A .knowledge
811
fi
@@ -29,3 +32,5 @@ if [ "${#too_large[@]}" -gt 0 ]; then
2932
printf ' - %s\n' "${too_large[@]}"
3033
exit 1
3134
fi
35+
36+
bash "$REPO_ROOT/scripts/pre-commit-secret-guard.sh"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"packages/*"
99
],
1010
"scripts": {
11+
"setup:pre-commit-hook": "node scripts/setup-pre-commit-hook.js",
1112
"build": "pnpm --filter ./packages/app build",
1213
"check": "pnpm --filter ./packages/app check && pnpm --filter ./packages/lib typecheck",
1314
"changeset": "changeset",

scripts/pre-commit-secret-guard.sh

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# CHANGE: Add bash-only pre-commit guard that redacts probable GitHub/OAuth secrets in staged files.
5+
# WHY: Avoid relying on Node runtime in hook execution and keep local push-protection checks deterministic.
6+
7+
ROOT_DIR="$(git rev-parse --show-toplevel)"
8+
cd "$ROOT_DIR"
9+
10+
command -v git >/dev/null || { echo "ERROR: git is required" >&2; exit 1; }
11+
command -v perl >/dev/null || { echo "ERROR: perl is required" >&2; exit 1; }
12+
13+
SECRET_PATTERN='\b(?:github_pat_|gho_|ghp_|ghu_|ghs_|ghr_|gha_)[A-Za-z0-9_]{20,255}\b'
14+
15+
redacted_count=0
16+
manual_fix_files=()
17+
has_staged_files=0
18+
19+
TMP_DIR=$(mktemp -d)
20+
trap 'rm -rf "$TMP_DIR"' EXIT
21+
22+
while IFS= read -r -d '' path; do
23+
if [ -z "$path" ]; then
24+
continue
25+
fi
26+
27+
if ! git cat-file -e ":$path" 2>/dev/null; then
28+
continue
29+
fi
30+
31+
has_staged_files=1
32+
tmp_path="${TMP_DIR}/entry"
33+
has_unstaged=false
34+
35+
if ! git diff --quiet -- "$path"; then
36+
has_unstaged=true
37+
fi
38+
39+
if [ "$has_unstaged" = true ]; then
40+
git cat-file -p ":$path" > "$tmp_path"
41+
42+
if grep -Pq "$SECRET_PATTERN" "$tmp_path"; then
43+
manual_fix_files+=("$path")
44+
fi
45+
46+
continue
47+
fi
48+
49+
if ! grep -Pq "$SECRET_PATTERN" "$path"; then
50+
continue
51+
fi
52+
53+
perl -0pi -e 's/\b(?:github_pat_|gho_|ghp_|ghu_|ghs_|ghr_|gha_)[A-Za-z0-9_]{20,255}\b/<REDACTED_GITHUB_TOKEN>/g' "$path"
54+
git add -- "$path"
55+
redacted_count=$((redacted_count + 1))
56+
done < <(git diff --cached --name-only --diff-filter=ACM -z)
57+
58+
if [ "$has_staged_files" -eq 0 ]; then
59+
exit 0
60+
fi
61+
62+
if [ "${#manual_fix_files[@]}" -gt 0 ]; then
63+
echo "ERROR: secret-like tokens found in staged versions with unstaged changes."
64+
echo "Please fix these files manually in index or clear unstaged changes, then commit again:"
65+
for file in "${manual_fix_files[@]}"; do
66+
echo " - $file"
67+
done
68+
echo "Hint: clean working tree for those files first (git restore --worktree -- <file> && git add -- <file>)."
69+
exit 1
70+
fi
71+
72+
if [ "$redacted_count" -gt 0 ]; then
73+
echo "pre-commit: auto-redacted secrets in $redacted_count staged file(s)."
74+
fi
75+
76+
exit 0

scripts/setup-pre-commit-hook.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env node
2+
3+
// CHANGE: Add repeatable pre-commit hook setup for secret auto-redaction
4+
// WHY: Keep secret scanning on every commit without one-time manual hook wiring.
5+
// SOURCE: n/a
6+
// PURITY: SHELL (git config + filesystem)
7+
8+
const fs = require("node:fs");
9+
const path = require("node:path");
10+
11+
const repoRoot = path.resolve(__dirname, "..");
12+
13+
const hooksDir = path.join(repoRoot, ".githooks");
14+
const hookPath = path.join(hooksDir, "pre-commit");
15+
16+
fs.mkdirSync(hooksDir, { recursive: true });
17+
fs.writeFileSync(
18+
hookPath,
19+
`#!/usr/bin/env bash
20+
set -euo pipefail
21+
22+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
23+
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
24+
cd "$REPO_ROOT"
25+
26+
node scripts/split-knowledge-large-files.js
27+
if [ -d ".knowledge" ]; then
28+
git add -A .knowledge
29+
fi
30+
31+
if [ -d ".knowlenge" ]; then
32+
git add -A .knowlenge
33+
fi
34+
35+
MAX_BYTES=$((99 * 1000 * 1000))
36+
too_large=()
37+
38+
while IFS= read -r -d '' path; do
39+
if ! git cat-file -e ":$path" 2>/dev/null; then
40+
continue
41+
fi
42+
size=$(git cat-file -s ":$path")
43+
if [ "$size" -gt "$MAX_BYTES" ]; then
44+
too_large+=("$path ($size bytes)")
45+
fi
46+
done < <(git diff --cached --name-only -z --diff-filter=ACM)
47+
48+
if [ "\${#too_large[@]}" -gt 0 ]; then
49+
echo "ERROR: Staged files exceed 99MB limit (99,000,000 bytes)."
50+
printf ' - %s\\n' "\${too_large[@]}"
51+
exit 1
52+
fi
53+
54+
bash "$REPO_ROOT/scripts/pre-commit-secret-guard.sh"
55+
`,
56+
"utf8"
57+
);
58+
59+
fs.chmodSync(hookPath, 0o755);
60+
61+
console.log(
62+
"Installed .githooks/pre-commit."
63+
);
64+
console.log("Enable it for this repository with: git config core.hooksPath .githooks");

0 commit comments

Comments
 (0)