From 7d414c937ea8255be7f658efb4e6b0acc034ee85 Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 23 Apr 2026 12:34:00 -0400 Subject: [PATCH 1/2] chore(claude): add public-surface-reminder hook + customer-name rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `PreToolUse` hook that 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 two rules freshly in mind: 1. No real customer or company names — use `Acme Corporation` (or `Acme Corp` shorthand) instead. No exceptions. 2. No internal work-item IDs or tracker URLs — no `SOC-123`, `ENG-456`, `ASK-789`, `linear.app`, `sentry.io`, etc. Attention priming, not enforcement. Exit code is always 0. The model applies 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. A denylist file is itself a leak. Recognition and replacement happen at write time, every time. Mirrors the matching change in socket-repo-template so this rule and hook propagate fleet-wide. Also adds the written rules to CLAUDE.md so they exist even when the hook is not installed. --- .../hooks/public-surface-reminder/README.md | 37 ++++++++ .../hooks/public-surface-reminder/index.mts | 86 +++++++++++++++++++ .../public-surface-reminder/package.json | 12 +++ .claude/settings.json | 9 ++ CLAUDE.md | 2 + 5 files changed, 146 insertions(+) create mode 100644 .claude/hooks/public-surface-reminder/README.md create mode 100644 .claude/hooks/public-surface-reminder/index.mts create mode 100644 .claude/hooks/public-surface-reminder/package.json diff --git a/.claude/hooks/public-surface-reminder/README.md b/.claude/hooks/public-surface-reminder/README.md new file mode 100644 index 000000000..8a2b058c9 --- /dev/null +++ b/.claude/hooks/public-surface-reminder/README.md @@ -0,0 +1,37 @@ +# 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 Corporation` (or + `Acme Corp` on subsequent references). 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. diff --git a/.claude/hooks/public-surface-reminder/index.mts b/.claude/hooks/public-surface-reminder/index.mts new file mode 100644 index 000000000..badb54ab7 --- /dev/null +++ b/.claude/hooks/public-surface-reminder/index.mts @@ -0,0 +1,86 @@ +#!/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 Corporation` +// (or `Acme Corp` shorthand) 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 Corporation` (or `Acme Corp`). 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() diff --git a/.claude/hooks/public-surface-reminder/package.json b/.claude/hooks/public-surface-reminder/package.json new file mode 100644 index 000000000..cee721e2c --- /dev/null +++ b/.claude/hooks/public-surface-reminder/package.json @@ -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" + } +} diff --git a/.claude/settings.json b/.claude/settings.json index ac130fc10..a28d95d99 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -9,6 +9,15 @@ "command": "node .claude/hooks/check-new-deps/index.mts" } ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node .claude/hooks/public-surface-reminder/index.mts" + } + ] } ] } diff --git a/CLAUDE.md b/CLAUDE.md index a5a213285..835f71197 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,8 @@ The umbrella rule: never run a git command that mutates state belonging to a pat - Never create files unless necessary; always prefer editing existing files - Forbidden to create docs unless requested - 🚨 **NEVER use `npx`, `pnpm dlx`, or `yarn dlx`** — use `pnpm exec ` or `pnpm run