Skip to content

Commit 6028cde

Browse files
malware-devclaude
andcommitted
feat: Refuse commits that name something internal
This repository is public. A doc comment and a commit message had already put a live resource name, an issue id and telemetry detail into it, none of which any reader outside the team should see. - A pre-commit and commit-msg hook scan added lines and the message for protected terms, email addresses and credential-shaped strings. Removals are ignored, so taking a leaked term out again is never blocked. - The list of real names lives in .git/leak-terms.txt and is never committed. A deny-list of what you are hiding, published, hands over the whole list. - A workflow runs the same script over every pushed commit, because a local hook is one --no-verify away from doing nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TExPjUDcSFMaH5cdgAmEvS
1 parent 7c07348 commit 6028cde

6 files changed

Lines changed: 155 additions & 0 deletions

File tree

.githooks/commit-msg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
# The message too. A commit message is as public as the diff and is far easier to write
3+
# carelessly, because nobody reviews it.
4+
"$(git rev-parse --show-toplevel)/.githooks/leak-scan.sh" < "$1" || exit 1

.githooks/leak-scan.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/sh
2+
# Refuses content that names things outside this repository: customer, employer or
3+
# infrastructure identifiers that have no business in a public history.
4+
#
5+
# Reads text on stdin. Exits non-zero and prints what matched.
6+
#
7+
# The list of real names lives in .git/leak-terms.txt, which is inside .git and therefore
8+
# never committed. That is deliberate: a deny-list of the words you are hiding, committed
9+
# to the repository, publishes exactly what it was meant to protect.
10+
11+
terms_file="$(git rev-parse --git-dir)/leak-terms.txt"
12+
13+
scan_input="$(mktemp)"
14+
cat > "$scan_input"
15+
trap 'rm -f "$scan_input"' EXIT
16+
17+
# Literal matching is done in the shell rather than with "grep -qF", which aborts on some
18+
# Git for Windows builds and takes the whole check down with it, silently passing.
19+
lower="$(tr '[:upper:]' '[:lower:]' < "$scan_input")"
20+
21+
found=""
22+
23+
if [ -f "$terms_file" ]; then
24+
while IFS= read -r term || [ -n "$term" ]; do
25+
case "$term" in ''|\#*) continue ;; esac
26+
lterm="$(printf '%s' "$term" | tr '[:upper:]' '[:lower:]')"
27+
case "$lower" in
28+
*"$lterm"*) found="$found
29+
names a protected term: $term" ;;
30+
esac
31+
done < "$terms_file"
32+
fi
33+
34+
# Built-in shapes, worth catching with no local list at all. Any noreply address is
35+
# excluded: git writes them into every trailer and they identify nobody.
36+
address="$(grep -oiE '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' "$scan_input" \
37+
| grep -viE '(^|[.+-])noreply@|@noreply\.' | head -1)"
38+
[ -n "$address" ] && found="$found
39+
contains an email address: $address"
40+
41+
if grep -qE '(gh[pousr]_[A-Za-z0-9]{16,}|sntrys_[A-Za-z0-9]{16,}|eyJ[A-Za-z0-9_-]{20,}\.)' "$scan_input"; then
42+
found="$found
43+
contains something shaped like a credential"
44+
fi
45+
46+
if [ -n "$found" ]; then
47+
printf 'Refused: this would publish something internal.%s\n' "$found"
48+
printf '\nReword it, or add a deliberate exception to .git/leak-terms.txt\n'
49+
exit 1
50+
fi
51+
exit 0

.githooks/leak-terms.example.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copy this to .git/leak-terms.txt and fill in the real values.
2+
#
3+
# That destination is inside .git, so it is never committed. This example lives in the
4+
# repository only to document the format: a deny-list naming the things you are keeping
5+
# out of a public history must not itself be published, or it hands over the whole list.
6+
#
7+
# One literal term per line, matched case-insensitively anywhere in the text.
8+
# '#' starts a comment. Blank lines are ignored.
9+
10+
# your-employer
11+
# your-customer
12+
# internal-hostname-fragment
13+
# -a-resource-naming-suffix
14+
# InternalServiceName

.githooks/pre-commit

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
# Everything about to enter history, scanned first.
3+
#
4+
# Added lines only. A diff carries removals too, and scanning those would refuse the one
5+
# commit you most need to make: the one that takes a leaked term back out again.
6+
git diff --cached -U0 \
7+
| grep '^+' \
8+
| grep -v '^+++' \
9+
| "$(git rev-parse --show-toplevel)/.githooks/leak-scan.sh" || exit 1

.github/workflows/leak-scan.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: leak-scan
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
scan:
9+
runs-on: ubuntu-latest
10+
name: No internal identifiers
11+
12+
steps:
13+
- uses: actions/checkout@v6
14+
with:
15+
fetch-depth: 0
16+
17+
# The literal deny-list cannot live in the repository, so CI carries it as a secret.
18+
# Absent, the built-in shapes still apply; they need no list.
19+
- name: Restore the deny-list
20+
env:
21+
LEAK_TERMS: ${{ secrets.LEAK_TERMS }}
22+
run: |
23+
if [ -n "$LEAK_TERMS" ]; then
24+
echo "$LEAK_TERMS" > "$(git rev-parse --git-dir)/leak-terms.txt"
25+
echo "deny-list restored"
26+
else
27+
echo "no LEAK_TERMS secret configured; built-in shapes only"
28+
fi
29+
30+
- name: Scan the pushed commits
31+
env:
32+
BEFORE: ${{ github.event.before }}
33+
HEAD_SHA: ${{ github.sha }}
34+
run: |
35+
chmod +x .githooks/leak-scan.sh
36+
37+
range="$HEAD_SHA~1..$HEAD_SHA"
38+
if git rev-parse --verify --quiet "$BEFORE^{commit}" >/dev/null; then
39+
range="$BEFORE..$HEAD_SHA"
40+
fi
41+
echo "Scanning $range"
42+
43+
failed=0
44+
for c in $(git rev-list "$range"); do
45+
if ! git log -1 --format='%B' "$c" | ./.githooks/leak-scan.sh; then
46+
echo "::error::commit message of $c names something internal"
47+
failed=1
48+
fi
49+
if ! git show "$c" -U0 --format= | grep '^+' | grep -v '^+++' | ./.githooks/leak-scan.sh; then
50+
echo "::error::content added by $c names something internal"
51+
failed=1
52+
fi
53+
done
54+
exit $failed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,29 @@ a request the running app picks up. If Huginn is not running, it says so rather
136136
Set `HUGINN_PROFILE` to keep a second install's settings, credentials and snapshot separate from
137137
the first.
138138

139+
## Keeping internal names out
140+
141+
This repository is public, so nothing in it may name a customer, an employer or their
142+
infrastructure. Two checks enforce that rather than leaving it to care.
143+
144+
Enable the hooks once per clone:
145+
146+
```bash
147+
git config core.hooksPath .githooks
148+
```
149+
150+
They refuse a commit whose **added lines or message** name a protected term, an email address
151+
or something shaped like a credential. Removals are ignored, so taking a leaked term back out
152+
is never blocked.
153+
154+
The list of real names lives in `.git/leak-terms.txt`, which is inside `.git` and so is never
155+
committed; `.githooks/leak-terms.example.txt` documents the format. A deny-list naming the
156+
things you are keeping out of a public history must not itself be published.
157+
158+
`.github/workflows/leak-scan.yml` runs the same script over every pushed commit, because a
159+
local hook is one `--no-verify` away from doing nothing. Give the repository a `LEAK_TERMS`
160+
secret to apply the literal list there too; without it the built-in shapes still apply.
161+
139162
## License
140163

141164
[MIT](LICENSE)

0 commit comments

Comments
 (0)