Skip to content
Merged
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
36 changes: 36 additions & 0 deletions .claude/hooks/public-surface-reminder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# public-surface-reminder

`PreToolUse` hook that **never blocks**. On every `Bash` command that would
publish text to a public Git/GitHub surface, writes a short reminder to
stderr so the model re-reads the command with the two rules freshly in
mind:

1. **No real customer or company names.** Use `Acme Inc`. No exceptions.
2. **No internal work-item IDs or tracker URLs.** No `SOC-123` /
`ENG-456` / `ASK-789` / similar, no `linear.app` / `sentry.io` URLs.

Attention priming, not enforcement. The model is responsible for actually
applying the rule — the hook just ensures the rule is in the active
context at the moment the command is about to fire.

## What counts as "public surface"

- `git commit` (including `--amend`)
- `git push`
- `gh pr (create|edit|comment|review)`
- `gh issue (create|edit|comment)`
- `gh api -X POST|PATCH|PUT`
- `gh release (create|edit)`

Any other `Bash` command passes through silently.

## Why no denylist

Because a denylist is itself a customer leak. A file named
`customers.txt` that enumerates "these are our customers" is worse than
the bug it tries to prevent. Recognition and replacement happen at write
time, done by the model, every time.

## Exit code

Always `0`. The hook prints a reminder and steps aside.
85 changes: 85 additions & 0 deletions .claude/hooks/public-surface-reminder/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node
// Claude Code PreToolUse hook — public-surface reminder.
//
// Never blocks. On every Bash command that would publish text to a public
// Git/GitHub surface (git commit, git push, gh pr/issue/api/release write),
// writes a short reminder to stderr so the model re-reads the command with
// the two rules freshly in mind:
//
// 1. No real customer/company names — ever. Use `Acme Inc` instead.
// 2. No internal work-item IDs or tracker URLs — no `SOC-123`, `ENG-456`,
// `ASK-789`, `linear.app`, `sentry.io`, etc.
//
// Exit code is always 0. This is attention priming, not enforcement. The
// model is responsible for actually applying the rule — the hook just makes
// sure the rule is in the active context at the moment the command is about
// to fire.
//
// Deliberately carries no list of customer names. Recognition and
// replacement happen at write time, not via enumeration.
//
// Reads a Claude Code PreToolUse JSON payload from stdin:
// { "tool_name": "Bash", "tool_input": { "command": "..." } }

import { readFileSync } from 'node:fs'

type ToolInput = {
tool_name?: string
tool_input?: {
command?: string
}
}

// Commands that can publish content outside the local machine.
// Keep broad — better to remind on an extra read than miss a write.
const PUBLIC_SURFACE_PATTERNS: RegExp[] = [
/\bgit\s+commit\b/,
/\bgit\s+push\b/,
/\bgh\s+pr\s+(create|edit|comment|review)\b/,
/\bgh\s+issue\s+(create|edit|comment)\b/,
/\bgh\s+api\b[^|]*-X\s*(POST|PATCH|PUT)\b/i,
/\bgh\s+release\s+(create|edit)\b/,
]

function isPublicSurface(command: string): boolean {
const normalized = command.replace(/\s+/g, ' ')
return PUBLIC_SURFACE_PATTERNS.some(re => re.test(normalized))
}

function main(): void {
let raw = ''
try {
raw = readFileSync(0, 'utf8')
} catch {
return
}

let input: ToolInput
try {
input = JSON.parse(raw)
} catch {
return
}

if (input.tool_name !== 'Bash') {
return
}
const command = input.tool_input?.command
if (!command || typeof command !== 'string') {
return
}
if (!isPublicSurface(command)) {
return
}

const lines = [
'[public-surface-reminder] This command writes to a public Git/GitHub surface.',
' • Re-read the commit message / PR body / comment BEFORE it sends.',
' • No real customer or company names — use `Acme Inc`. No exceptions.',
' • No internal work-item IDs or tracker URLs (linear.app, sentry.io, SOC-/ENG-/ASK-/etc.).',
' • If you spot one, cancel and rewrite the text first.',
]
process.stderr.write(lines.join('\n') + '\n')
}

main()
12 changes: 12 additions & 0 deletions .claude/hooks/public-surface-reminder/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@socketsecurity/hook-public-surface-reminder",
"private": true,
"type": "module",
"main": "./index.mts",
"exports": {
".": "./index.mts"
},
"devDependencies": {
"@types/node": "24.9.2"
}
}
57 changes: 57 additions & 0 deletions .claude/hooks/token-hygiene/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# token-hygiene

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`.

## What it blocks

| Rule | Example | Fix |
|------|---------|-----|
| Literal token in command | `echo vtwn_abc123…` | Rotate the exposed token; read tokens from `.env.local` at spawn time, never inline them |
| `env`/`printenv`/`export -p`/`set` dumping everything | `env \| grep FOO` (unredacted) | `env \| sed 's/=.*/=<redacted>/'` or filter specific keys |
| `.env*` read without redactor | `cat .env.local` | `sed 's/=.*/=<redacted>/' .env.local` or `grep -v '^#' .env.local \| cut -d= -f1` |
| `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` |
| References sensitive env var name writing unredacted to stdout | `echo $API_KEY` | Same as above |

## What it allows

- Any write to a file (`>`, `>>`, `tee`)
- Any pipe through `jq`, `grep`, `head`, `tail`, `wc`, `cut`, `awk`, `sed s/=.*/=<redacted>/`, `python3 -m json.tool`
- Legitimate `git`/`pnpm`/`npm`/`node`/`tsc`/`oxfmt`/`oxlint` invocations that happen to reference env var names but don't echo values
- Any curl call that does not carry an `Authorization:` header

## Detected token shapes

Literal value patterns caught in-command:

- Val Town — `vtwn_`
- Linear — `lin_api_`
- OpenAI / Anthropic — `sk-` (20+ chars)
- Stripe — `sk_live_`, `sk_test_`, `pk_live_`, `rk_live_`
- GitHub — `ghp_`, `gho_`, `ghs_`, `ghu_`, `ghr_`, `github_pat_`
- GitLab — `glpat-`
- AWS — `AKIA…`
- Slack — `xoxb-`, `xoxa-`, `xoxp-`, `xoxr-`, `xoxs-`
- Google — `AIza…`
- JWTs — three-segment `eyJ…`

## Control flow

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.

## Testing

```bash
pnpm --filter @socketsecurity/hook-token-hygiene test
```

Adding new token-shape detections: update `LITERAL_TOKEN_PATTERNS` in `index.mts`, add a positive and negative test in `test/token-hygiene.test.mts`.

## Updating across the fleet

This file is in `IDENTICAL_FILES` in `scripts/sync-scaffolding.mjs`. After editing, run from `socket-repo-template`:

```bash
node scripts/sync-scaffolding.mjs --all --fix
```

to propagate the change to every fleet repo.
Loading