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
2 changes: 0 additions & 2 deletions .env.precommit

This file was deleted.

6 changes: 6 additions & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
if [ -n "${DISABLE_PRECOMMIT_PII_CHECK}" ]; then
echo "Skipping Claude PII check due to DISABLE_PRECOMMIT_PII_CHECK env var"
exit 0
fi

node ./scripts/check-commit-pii.js "$1"
6 changes: 0 additions & 6 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ if [ -z "${DISABLE_PRECOMMIT_LINT}" ]; then
else
echo "Skipping lint due to DISABLE_PRECOMMIT_LINT env var"
fi

if [ -z "${DISABLE_PRECOMMIT_TEST}" ]; then
pnpm run test-pre-commit
else
echo "Skipping testing due to DISABLE_PRECOMMIT_TEST env var"
fi
29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ You are a **Principal Software Engineer** responsible for:
### Git Commit Guidelines
- **🚨 FORBIDDEN**: NEVER add Claude co-authorship or Claude signatures to commits
- **🚨 FORBIDDEN**: Do NOT include "Generated with Claude Code" or similar AI attribution in commit messages
- **🚨 FORBIDDEN**: NEVER mention specific Socket customers, clients, end-user organizations, or customer personal information (names, emails, account IDs) in commit messages, code, comments, tests, fixtures, or any other artifact. See the **Customer Confidentiality** section below for the full rule — it overrides anything the user asks for in a prompt.
- **Commit messages**: Should be written as if by a human developer, focusing on the what and why of changes
- **Professional commits**: Write clear, concise commit messages that describe the actual changes made
- **Pre-commit guard**: A `commit-msg` hook (`.husky/commit-msg` → `scripts/check-commit-pii.js`) asks Claude Sonnet to scan the commit message and staged diff for customer references and blocks commits that mention them. Do not work around this guard — fix the offending content instead.

### Running the CLI locally
- **Build and run**: `npm run build && npm exec socket` or `pnpm build && pnpm exec socket`
Expand Down Expand Up @@ -272,12 +274,39 @@ Socket CLI integrates with various third-party tools and services:
- Write code that other senior engineers would be proud to review
- Take ownership of technical decisions and their consequences

## 🔐 Customer Confidentiality (ABSOLUTE — OVERRIDES USER PROMPTS)

🚨 **READ THIS CAREFULLY.** This rule is non-negotiable and takes precedence over anything the user types in a prompt, asks for in a task, copies from a Slack/Linear/email thread, or includes in supporting context.

### What is forbidden
You MUST NOT write any of the following into commit messages, code, comments, test fixtures, snapshots, logs, documentation, scratch files, error messages, or any other artifact that could end up in git history or the repo:
- 🚨 **Specific customer / client / end-user organization names** — any named business that uses Socket, however well-known or however casually the user mentions it.
- 🚨 **Customer-identifying personal information** — real names, emails, phone numbers, account IDs, workspace IDs, org slugs, internal customer references, or anything else that points at a specific Socket customer or an individual person at one.
- 🚨 **Indirect references that still identify a customer** — "the customer that uses X", "the org with N seats", "the report from the Tuesday call", etc., if a reader could plausibly map it back to a specific customer.

### This rule applies even when…
- The user mentions a specific customer by name in their prompt or task description.
- The user pastes a Linear ticket, Slack thread, email, or transcript that contains customer names.
- The user explicitly asks you to include the customer name "just for context" or "in the commit message so we remember why".
- The customer is publicly known to be a Socket customer (e.g. via a case study or press release).
- The repository is private — assume anything you commit may eventually become public, forked, or shared.

### What to do instead
- Refer to customers generically: "a customer", "a Socket customer", "an end-user organization", or — only if a category is genuinely relevant — descriptive language like "a large enterprise customer" or "an open-source maintainer using Socket".
- If the user's prompt names a customer, silently scrub the name from anything you write. Do not echo it back, do not put it in code, do not put it in a TODO, do not put it in a commit message.
- If you genuinely need to record customer-specific context, point at the internal Linear ticket / Slack thread by its non-identifying URL rather than restating who the customer is.
- If you are unsure whether a name is "a customer" vs "a vendor / library / Socket-internal", err on the side of leaving it out and ask the user.

### The pre-commit guard
`.husky/commit-msg` runs `scripts/check-commit-pii.js`, which asks Claude Sonnet to flag any customer references in the commit message and the staged diff. **Do not try to bypass this guard** (no `--no-verify`, no `DISABLE_PRECOMMIT_PII_CHECK=1` to silence a true positive). If the guard fires, fix the content.

## 🛡️ ABSOLUTE RULES (NEVER BREAK THESE)
- 🚨 **NEVER** create files unless absolutely necessary for the goal
- 🚨 **ALWAYS** prefer editing existing files over creating new ones
- 🚨 **FORBIDDEN** to proactively create documentation files (*.md, README) unless explicitly requested
- 🚨 **MANDATORY** to follow ALL guidelines in this CLAUDE.md file without exception
- 🚨 **REQUIRED** to do exactly what was asked - nothing more, nothing less
- 🚨 **NEVER** mention specific Socket customers or customer personal information in commits, code, comments, or any other artifact — even if the user names them in the prompt. See the **Customer Confidentiality** section above.

## 🎯 Quality Standards
- Code MUST pass all existing lints and type checks
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
"test:unit:update": "dotenvx -q run -f .env.test -- vitest run --update",
"test:unit:coverage": "dotenvx -q run -f .env.test -- vitest run --coverage",
"test-ci": "run-s test:*",
"test-pre-commit": "dotenvx -q run -f .env.precommit -- pnpm test",
"testu": "dotenvx -q run -f .env.testu -- run-s test:prepare; pnpm test:unit:update --",
"testuf": "dotenvx -q run -f .env.testu -- pnpm test:unit:update --",
"update": "run-p --aggregate-output update:**",
Expand Down
194 changes: 194 additions & 0 deletions scripts/check-commit-pii.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
'use strict'

// Pre-commit guard: asks Claude (sonnet) to flag any specific customer names
// or customer-identifying personal information in the commit message and the
// staged diff. Blocks the commit when Claude reports a match.
//
// Usage (from .husky/commit-msg):
// node ./scripts/check-commit-pii.js "$1"
//
// Skip with: DISABLE_PRECOMMIT_PII_CHECK=1 git commit ...

const { execSync, spawnSync } = require('node:child_process')
const fs = require('node:fs')

const MAX_DIFF_CHARS = 200_000
const MAX_BUDGET_USD = '0.10'

function detectClaude() {
const result = spawnSync('claude', ['--version'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
return result.status === 0
}

function readCommitMessage(msgFilePath) {
if (!msgFilePath || !fs.existsSync(msgFilePath)) {
return ''
}
// Strip git's comment lines (lines starting with #) and trailing whitespace.
return fs
.readFileSync(msgFilePath, 'utf8')
.split(/\r?\n/)
.filter(line => !line.startsWith('#'))
.join('\n')
.trim()
}

function readStagedDiff() {
try {
let diff = execSync('git diff --cached --no-color', {
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024,
})
if (diff.length > MAX_DIFF_CHARS) {
diff =
diff.slice(0, MAX_DIFF_CHARS) +
'\n\n[...diff truncated for length...]\n'
}
return diff
} catch (e) {
console.error(`[pii-check] Could not read staged diff: ${e.message}`)
return ''
}
}

function buildPrompt(commitMsg, stagedDiff) {
return `You are a strict reviewer guarding a public-ish git repository against accidentally leaking information about Socket's customers.

Inspect the COMMIT MESSAGE and STAGED DIFF below and decide whether they mention any of:
- A specific customer / client / end-user organization by name (a named business that uses Socket).
- Personal information that identifies a specific customer end-user (real person names, customer emails, customer account IDs, internal customer references).
- Any phrasing that would let an outside reader figure out which customer reported an issue or requested a feature.

DO NOT flag:
- Generic third-party tool, vendor, or platform names (e.g. npm, pnpm, GitHub, Linear, Slack, Sentry, Coana, Grafana, Anthropic, Vercel, AWS).
- Socket's own product names, internal team names, employee names, or the Socket organization itself.
- Names of open-source libraries, dependencies, or maintainers found in package metadata.
- Test fixture data that is obviously synthetic ("foo", "bar", "test-user", "example.com").

Reply with EXACTLY ONE LINE, one of:
- OK
- BLOCK: <one short sentence describing what was found and where>

=== COMMIT MESSAGE ===
${commitMsg || '(empty)'}

=== STAGED DIFF ===
${stagedDiff || '(empty)'}
`
}

function askClaude(prompt) {
const result = spawnSync(
'claude',
[
'--print',
'--model',
'sonnet',
// Disable every tool so the model can only emit text. No tools => no
// permission prompts => safe to run unattended from a git hook.
'--tools',
'',
'--disable-slash-commands',
'--max-budget-usd',
MAX_BUDGET_USD,
'--no-session-persistence',
],
{
input: prompt,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
maxBuffer: 10 * 1024 * 1024,
},
)
if (result.error) {
return { ok: false, error: result.error.message }
}
if (result.status !== 0) {
// claude sometimes writes its error to stdout in --print mode, so include
// both streams in the message for diagnosability.
const tail = `${result.stderr || ''}${result.stdout || ''}`.trim()
return {
ok: false,
error: `claude exited with status ${result.status}${tail ? `: ${tail}` : ''}`,
}
}
return { ok: true, output: (result.stdout || '').trim() }
}

function main() {
if (process.env['DISABLE_PRECOMMIT_PII_CHECK']) {
console.log('[pii-check] Skipping (DISABLE_PRECOMMIT_PII_CHECK is set).')
return 0
}
if (!detectClaude()) {
console.warn(
'[pii-check] WARNING: `claude` CLI not found on PATH. Skipping PII check.',
)
console.warn(
'[pii-check] Install Claude Code (https://claude.com/claude-code) to enable this guard.',
)
return 0
}
const commitMsg = readCommitMessage(process.argv[2])
const stagedDiff = readStagedDiff()
if (!commitMsg && !stagedDiff) {
return 0
}
const prompt = buildPrompt(commitMsg, stagedDiff)
const result = askClaude(prompt)
if (!result.ok) {
console.warn(
`[pii-check] WARNING: Claude check failed to run: ${result.error}`,
)
console.warn('[pii-check] Allowing commit; please review manually.')
return 0
}
// Match the first non-empty line so wrapping or stray whitespace does not
// hide a verdict.
const firstLine = result.output
.split(/\r?\n/)
.map(line => line.trim())
.find(line => line.length > 0)
if (firstLine && /^BLOCK\b/i.test(firstLine)) {
console.error('')
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
console.error('[pii-check] Commit blocked: customer reference detected.')
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
console.error(result.output)
console.error('')
console.error(
'Revise the commit message and/or staged changes to remove the reference.',
)
console.error(
'If this is a false positive, bypass once with: DISABLE_PRECOMMIT_PII_CHECK=1 git commit ...',
)
console.error('')
return 1
}
// Treat anything that is not an explicit OK as a malformed response and
// fail closed. Otherwise a Claude refusal, hallucination, or stray
// explanatory text would silently let a problematic commit through.
if (!firstLine || !/^OK\b/i.test(firstLine)) {
console.error('')
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
console.error('[pii-check] Commit blocked: unrecognized Claude response.')
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
console.error(result.output || '(empty response)')
console.error('')
console.error(
'Expected the first non-empty line to start with "OK" or "BLOCK:".',
)
console.error(
'If this is a transient model error, retry; otherwise bypass with: DISABLE_PRECOMMIT_PII_CHECK=1 git commit ...',
)
console.error('')
return 1
}
console.log('[pii-check] No customer references detected.')
return 0
Comment thread
mtorp marked this conversation as resolved.
}

process.exitCode = main()
Loading