Skip to content

Commit f9492c2

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/cdxgen-empty-components
2 parents d45a4a9 + 7641bf4 commit f9492c2

39 files changed

Lines changed: 2335 additions & 118 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# public-surface-reminder
2+
3+
`PreToolUse` hook that **never blocks**. On every `Bash` command that would
4+
publish text to a public Git/GitHub surface, writes a short reminder to
5+
stderr so the model re-reads the command with the two rules freshly in
6+
mind:
7+
8+
1. **No real customer or company names.** Use `Acme Inc`. No exceptions.
9+
2. **No internal work-item IDs or tracker URLs.** No `SOC-123` /
10+
`ENG-456` / `ASK-789` / similar, no `linear.app` / `sentry.io` URLs.
11+
12+
Attention priming, not enforcement. The model is responsible for actually
13+
applying the rule — the hook just ensures the rule is in the active
14+
context at the moment the command is about to fire.
15+
16+
## What counts as "public surface"
17+
18+
- `git commit` (including `--amend`)
19+
- `git push`
20+
- `gh pr (create|edit|comment|review)`
21+
- `gh issue (create|edit|comment)`
22+
- `gh api -X POST|PATCH|PUT`
23+
- `gh release (create|edit)`
24+
25+
Any other `Bash` command passes through silently.
26+
27+
## Why no denylist
28+
29+
Because a denylist is itself a customer leak. A file named
30+
`customers.txt` that enumerates "these are our customers" is worse than
31+
the bug it tries to prevent. Recognition and replacement happen at write
32+
time, done by the model, every time.
33+
34+
## Exit code
35+
36+
Always `0`. The hook prints a reminder and steps aside.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
// Claude Code PreToolUse hook — public-surface reminder.
3+
//
4+
// Never blocks. On every Bash command that would publish text to a public
5+
// Git/GitHub surface (git commit, git push, gh pr/issue/api/release write),
6+
// writes a short reminder to stderr so the model re-reads the command with
7+
// the two rules freshly in mind:
8+
//
9+
// 1. No real customer/company names — ever. Use `Acme Inc` instead.
10+
// 2. No internal work-item IDs or tracker URLs — no `SOC-123`, `ENG-456`,
11+
// `ASK-789`, `linear.app`, `sentry.io`, etc.
12+
//
13+
// Exit code is always 0. This is attention priming, not enforcement. The
14+
// model is responsible for actually applying the rule — the hook just makes
15+
// sure the rule is in the active context at the moment the command is about
16+
// to fire.
17+
//
18+
// Deliberately carries no list of customer names. Recognition and
19+
// replacement happen at write time, not via enumeration.
20+
//
21+
// Reads a Claude Code PreToolUse JSON payload from stdin:
22+
// { "tool_name": "Bash", "tool_input": { "command": "..." } }
23+
24+
import { readFileSync } from 'node:fs'
25+
26+
type ToolInput = {
27+
tool_name?: string
28+
tool_input?: {
29+
command?: string
30+
}
31+
}
32+
33+
// Commands that can publish content outside the local machine.
34+
// Keep broad — better to remind on an extra read than miss a write.
35+
const PUBLIC_SURFACE_PATTERNS: RegExp[] = [
36+
/\bgit\s+commit\b/,
37+
/\bgit\s+push\b/,
38+
/\bgh\s+pr\s+(create|edit|comment|review)\b/,
39+
/\bgh\s+issue\s+(create|edit|comment)\b/,
40+
/\bgh\s+api\b[^|]*-X\s*(POST|PATCH|PUT)\b/i,
41+
/\bgh\s+release\s+(create|edit)\b/,
42+
]
43+
44+
function isPublicSurface(command: string): boolean {
45+
const normalized = command.replace(/\s+/g, ' ')
46+
return PUBLIC_SURFACE_PATTERNS.some(re => re.test(normalized))
47+
}
48+
49+
function main(): void {
50+
let raw = ''
51+
try {
52+
raw = readFileSync(0, 'utf8')
53+
} catch {
54+
return
55+
}
56+
57+
let input: ToolInput
58+
try {
59+
input = JSON.parse(raw)
60+
} catch {
61+
return
62+
}
63+
64+
if (input.tool_name !== 'Bash') {
65+
return
66+
}
67+
const command = input.tool_input?.command
68+
if (!command || typeof command !== 'string') {
69+
return
70+
}
71+
if (!isPublicSurface(command)) {
72+
return
73+
}
74+
75+
const lines = [
76+
'[public-surface-reminder] This command writes to a public Git/GitHub surface.',
77+
' • Re-read the commit message / PR body / comment BEFORE it sends.',
78+
' • No real customer or company names — use `Acme Inc`. No exceptions.',
79+
' • No internal work-item IDs or tracker URLs (linear.app, sentry.io, SOC-/ENG-/ASK-/etc.).',
80+
' • If you spot one, cancel and rewrite the text first.',
81+
]
82+
process.stderr.write(lines.join('\n') + '\n')
83+
}
84+
85+
main()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@socketsecurity/hook-public-surface-reminder",
3+
"private": true,
4+
"type": "module",
5+
"main": "./index.mts",
6+
"exports": {
7+
".": "./index.mts"
8+
},
9+
"devDependencies": {
10+
"@types/node": "24.9.2"
11+
}
12+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# token-hygiene
2+
3+
Claude Code `PreToolUse` hook that refuses Bash tool calls that would leak secrets to tool output. Mandatory across the Socket fleet — every repo ships this file byte-for-byte via `scripts/sync-scaffolding.mjs`.
4+
5+
## What it blocks
6+
7+
| Rule | Example | Fix |
8+
|------|---------|-----|
9+
| Literal token in command | `echo vtwn_abc123…` | Rotate the exposed token; read tokens from `.env.local` at spawn time, never inline them |
10+
| `env`/`printenv`/`export -p`/`set` dumping everything | `env \| grep FOO` (unredacted) | `env \| sed 's/=.*/=<redacted>/'` or filter specific keys |
11+
| `.env*` read without redactor | `cat .env.local` | `sed 's/=.*/=<redacted>/' .env.local` or `grep -v '^#' .env.local \| cut -d= -f1` |
12+
| `curl -H "Authorization:"` with unfiltered stdout | `curl -H "Authorization: Bearer $TOKEN" api.example.com` | Redirect to file/`/dev/null`, or pipe to `jq`/`grep`/`head`/`wc`/`cut`/`awk` |
13+
| References sensitive env var name writing unredacted to stdout | `echo $API_KEY` | Same as above |
14+
15+
## What it allows
16+
17+
- Any write to a file (`>`, `>>`, `tee`)
18+
- Any pipe through `jq`, `grep`, `head`, `tail`, `wc`, `cut`, `awk`, `sed s/=.*/=<redacted>/`, `python3 -m json.tool`
19+
- Legitimate `git`/`pnpm`/`npm`/`node`/`tsc`/`oxfmt`/`oxlint` invocations that happen to reference env var names but don't echo values
20+
- Any curl call that does not carry an `Authorization:` header
21+
22+
## Detected token shapes
23+
24+
Literal value patterns caught in-command:
25+
26+
- Val Town — `vtwn_`
27+
- Linear — `lin_api_`
28+
- OpenAI / Anthropic — `sk-` (20+ chars)
29+
- Stripe — `sk_live_`, `sk_test_`, `pk_live_`, `rk_live_`
30+
- GitHub — `ghp_`, `gho_`, `ghs_`, `ghu_`, `ghr_`, `github_pat_`
31+
- GitLab — `glpat-`
32+
- AWS — `AKIA…`
33+
- Slack — `xoxb-`, `xoxa-`, `xoxp-`, `xoxr-`, `xoxs-`
34+
- Google — `AIza…`
35+
- JWTs — three-segment `eyJ…`
36+
37+
## Control flow
38+
39+
The hook reads the tool-use payload from stdin, type-checks `tool_name === 'Bash'`, and runs `check(command)`. Any rule violation `throw`s a typed `BlockError`; a single top-level `try/catch` in `main()` writes the block message to stderr and sets `process.exitCode = 2`. Hook bugs fail **open** — a crash in the hook writes a log line and returns exit 0 so legitimate work isn't blocked on a bad deploy.
40+
41+
## Testing
42+
43+
```bash
44+
pnpm --filter @socketsecurity/hook-token-hygiene test
45+
```
46+
47+
Adding new token-shape detections: update `LITERAL_TOKEN_PATTERNS` in `index.mts`, add a positive and negative test in `test/token-hygiene.test.mts`.
48+
49+
## Updating across the fleet
50+
51+
This file is in `IDENTICAL_FILES` in `scripts/sync-scaffolding.mjs`. After editing, run from `socket-repo-template`:
52+
53+
```bash
54+
node scripts/sync-scaffolding.mjs --all --fix
55+
```
56+
57+
to propagate the change to every fleet repo.

0 commit comments

Comments
 (0)