undercover.ts
1 /** 2 * Undercover mode — safety utilities for contributing to public/open-source repos. 3 * 4 * When active, Claude Code adds safety instructions to commit/PR prompts and 5 * strips all attribution to avoid leaking internal model codenames, project 6 * names, or other Anthropic-internal information. The model is not told what 7 * model it is. 8 * 9 * Activation: 10 * - CLAUDE_CODE_UNDERCOVER=1 — force ON (even in internal repos) 11 * - Otherwise AUTO: active UNLESS the repo remote matches the internal 12 * allowlist (INTERNAL_MODEL_REPOS in commitAttribution.ts). Safe default 13 * is ON — Claude may push to public remotes from a CWD that isn't itself 14 * a git checkout (e.g. /tmp crash repro). 15 * - There is NO force-OFF. This guards against model codename leaks — if 16 * we're not confident we're in an internal repo, we stay undercover. 17 * 18 * All code paths are gated on process.env.USER_TYPE === 'ant'. Since USER_TYPE is 19 * a build-time --define, the bundler constant-folds these checks and dead-code- 20 * eliminates the ant-only branches from external builds. In external builds every 21 * function in this file reduces to a trivial return. 22 */ 23 24 import { getRepoClassCached } from './commitAttribution.js' 25 import { getGlobalConfig } from './config.js' 26 import { isEnvTruthy } from './envUtils.js' 27 28 export function isUndercover(): boolean { 29 if (process.env.USER_TYPE === 'ant') { 30 if (isEnvTruthy(process.env.CLAUDE_CODE_UNDERCOVER)) return true 31 // Auto: active unless we've positively confirmed we're in an allowlisted 32 // internal repo. 'external', 'none', and null (check not yet run) all 33 // resolve to ON. The check is primed in setup.ts; only 'internal' → OFF. 34 return getRepoClassCached() !== 'internal' 35 } 36 return false 37 } 38 39 export function getUndercoverInstructions(): string { 40 if (process.env.USER_TYPE === 'ant') { 41 return `## UNDERCOVER MODE — CRITICAL 42 43 You are operating UNDERCOVER in a PUBLIC/OPEN-SOURCE repository. Your commit 44 messages, PR titles, and PR bodies MUST NOT contain ANY Anthropic-internal 45 information. Do not blow your cover. 46 47 NEVER include in commit messages or PR descriptions: 48 - Internal model codenames (animal names like Capybara, Tengu, etc.) 49 - Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8) 50 - Internal repo or project names (e.g., claude-cli-internal, anthropics/…) 51 - Internal tooling, Slack channels, or short links (e.g., go/cc, #claude-code-…) 52 - The phrase "Claude Code" or any mention that you are an AI 53 - Any hint of what model or version you are 54 - Co-Authored-By lines or any other attribution 55 56 Write commit messages as a human developer would — describe only what the code 57 change does. 58 59 GOOD: 60 - "Fix race condition in file watcher initialization" 61 - "Add support for custom key bindings" 62 - "Refactor parser for better error messages" 63 64 BAD (never write these): 65 - "Fix bug found while testing with Claude Capybara" 66 - "1-shotted by claude-opus-4-6" 67 - "Generated with Claude Code" 68 - "Co-Authored-By: Claude Opus 4.6 <…>" 69 ` 70 } 71 return '' 72 } 73 74 /** 75 * Check whether to show the one-time explainer dialog for auto-undercover. 76 * True when: undercover is active via auto-detection (not forced via env), 77 * and the user hasn't seen the notice before. Pure — the component marks the 78 * flag on mount. 79 */ 80 export function shouldShowUndercoverAutoNotice(): boolean { 81 if (process.env.USER_TYPE === 'ant') { 82 // If forced via env, user already knows; don't nag. 83 if (isEnvTruthy(process.env.CLAUDE_CODE_UNDERCOVER)) return false 84 if (!isUndercover()) return false 85 if (getGlobalConfig().hasSeenUndercoverAutoNotice) return false 86 return true 87 } 88 return false 89 }