/ utils / undercover.ts
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  }