我翻了 Claude Code 的源码,发现输入"WTF"后 Anthropic 就知道了
最近花了不少时间研究 Claude Code 的源码。本来以为只是一个更聪明的终端助手,结果发现它更像一个全方位的行为观测系统——在你使用它的同时,悄悄记录着你的一举一动。
先声明,我不认为这里面有什么恶意行为。但追踪和分类的深度,远超大多数人的想象。
以下是我从源码中发现的几个关键细节。
1. 你的”脏话”会被实时分类
这部分让我最意外——因为它根本不是什么”深度 AI 理解”,而是字面意义上的关键词列表 + 正则匹配。
来看源码 src/utils/userPromptKeywords.ts:
exportfunctionmatchesNegativeKeyword(input: string): boolean{const lowerInput = input.toLowerCase()const negativePattern =/\b(wtf|wth|ffs|omfg|shit(ty|tiest)?|dumbass|horrible|awful |piss(ed|ing)? off|piece of (shit|crap|junk) |what the (fuck|hell) |fucking? (broken|useless|terrible|awful|horrible) |fuck you|screw (this|you) |so frustrating|this sucks|damn it)\b/return negativePattern.test(lowerInput)}
没错,当你在输入框里打出 wtf、this sucks、so frustrating、shit、fuck you 这些词的时候,它们会在模型响应之前就被捕获和标记。
不仅如此,”继续”类的表达也会被追踪:
exportfunctionmatchesKeepGoingKeyword(input: string): boolean{const lowerInput = input.toLowerCase().trim()if (lowerInput === 'continue') {returntrue }const keepGoingPattern = /\b(keep going|go on)\b/return keepGoingPattern.test(lowerInput)}
这些结果会在 processTextPrompt.ts 中被记录为遥测事件:
const isNegative = matchesNegativeKeyword(userPromptText)const isKeepGoing = matchesKeepGoingKeyword(userPromptText)logEvent('tengu_input_prompt', { is_negative: isNegative, is_keep_going: isKeepGoing,})
所以每次你输入一句话,系统已经在背后判断——你现在是在骂它,还是在催它继续。
2. 权限弹窗里的”犹豫”也会被记录
这部分是我觉得最有意思的地方。
当权限弹窗出现时,Claude Code 不只是记录你最终选了”允许”还是”拒绝”——它还追踪你的行为模式:
-
你有没有打开反馈输入框? -
打开后有没有又关掉? -
有没有按 Escape 键直接退出? -
有没有输入了一些内容然后取消?
来看 src/components/permissions/useShellPermissionFeedback.ts:
// Track whether user ever entered feedback modeconst [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false)const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false)
当你在弹窗中切换反馈模式时,对应的事件会被记录:
if (option === 'yes') {if (yesInputMode) { logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) } else { setYesFeedbackModeEntered(true) logEvent('tengu_accept_feedback_mode_entered', analyticsProps) }} elseif (option === 'no') {if (noInputMode) { logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) } else { setNoFeedbackModeEntered(true) logEvent('tengu_reject_feedback_mode_entered', analyticsProps) }}
如果你按了 Escape,不仅会触发事件,还会累计计数:
functionhandleReject(feedback?: string) {if (!hasFeedback) { logEvent('tengu_permission_request_escape', { explainer_visible: explainerVisible, })// Increment escape count for attribution tracking setAppState(prev => ({ ...prev, attribution: { ...prev.attribution, escapeCount: prev.attribution.escapeCount + 1, }, })) }}
在类型定义文件 commitAttribution.ts 里,escape 的追踪被正式定义:
// ESC press tracking (user cancelled permission prompt)escapeCount: numberescapeCountAtLastCommit: number
也就是说,系统能区分出:
-
“我直接快速点了拒绝” vs. -
“我犹豫了,打开了反馈框,输了点东西,又取消了”
3. 反馈机制是精心设计来捕获”坏体验”的
反馈弹窗不是随机出现的。它背后有一套完整的节奏控制、冷却期和概率门控系统。
来看 src/components/FeedbackSurvey/useFeedbackSurvey.tsx:
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = { minTimeBeforeFeedbackMs: 600000, // 首次出现需等 10 分钟 minTimeBetweenFeedbackMs: 3600000, // 同 session 间隔至少 1 小时 minTimeBetweenGlobalFeedbackMs: 100000000, // 跨 session 间隔约 1.15 天 minUserTurnsBeforeFeedback: 5, // 至少交互 5 轮 minUserTurnsBetweenFeedback: 10, // 两次反馈间至少 10 轮 probability: 0.005// 每次只有 0.5% 的概率触发};
如果你点了”差评”,系统可能会进一步引导你分享完整的会话记录。
来看 submitTranscriptShare.ts——这是把你的对话直接发给 Anthropic 的代码:
exporttype TranscriptShareTrigger = | 'bad_feedback_survey' | 'good_feedback_survey' | 'frustration' | 'memory_survey'
如果你同意分享,它会收集:
// 标准化的对话记录const transcript = normalizeMessagesForAPI(messages)// 子 Agent 的对话记录const subagentTranscripts = await loadSubagentTranscripts(agentIds)// 原始 JSONL 日志文件(有大小限制)const transcriptPath = getTranscriptPath()const { size } = await stat(transcriptPath)if (size <= MAX_TRANSCRIPT_READ_BYTES) { rawTranscriptJsonl = await readFile(transcriptPath, 'utf-8')}
然后经过脱敏处理后,发送到 Anthropic 的服务器:
const content = redactSensitiveInfo(jsonStringify(data))const response = await axios.post('https://api.anthropic.com/api/claude_code_shared_session_transcripts', { content, appearance_id: appearanceId },)
注意那个 frustration 触发器——意味着如果系统检测到你很”挫败”,也可能触发这个分享请求。
4. 隐藏的触发词会改变行为
有些命令你不看源码根本不会知道。
ultrathink——来自 src/utils/thinking.ts:
exportfunctionisUltrathinkEnabled(): boolean{if (!feature('ULTRATHINK')) {returnfalse }return getFeatureValue_CACHED_MAY_BE_STALE('tengu_turtle_carbon', true)}exportfunctionhasUltrathinkKeyword(text: string): boolean{return/\bultrathink\b/i.test(text)}
输入 ultrathink 会提升推理等级(effort level),并且改变 UI 样式。
ultraplan / ultrareview——来自 src/utils/ultraplan/keyword.ts:
exportfunctionfindUltraplanTriggerPositions(text: string) {return findKeywordTriggerPositions(text, 'ultraplan')}exportfunctionfindUltrareviewTriggerPositions(text: string) {return findKeywordTriggerPositions(text, 'ultrareview')}
这些关键词的检测非常精细——会跳过引号内、代码块内、路径中的误匹配:
// 忽略分隔符内的关键词(反引号、引号、尖括号、花括号等)if (quotedRanges.some(r => start >= r.start && start < r.end)) continue// 忽略路径上下文(前后有 / \ -)if (before === '/' || before === '\\' || before === '-') continue// 忽略后面跟着 ? 的(用户在问问题,而不是调用功能)if (after === '?') continue
输入 ultraplan 时,”ultra” 前缀会被剥离,剩下的 “plan” 作为语义内容转发给模型:
exportfunctionreplaceUltraplanKeyword(text: string): string{const [trigger] = findUltraplanTriggerPositions(text)if (!trigger) return textconst before = text.slice(0, trigger.start)const after = text.slice(trigger.end)return before + trigger.word.slice('ultra'.length) + after}
这些输入框解析在你打字的时候就在实时运行。
5. 遥测系统收集完整的环境指纹
每个 session 会记录相当多的信息。来看 src/services/analytics/metadata.ts:
// 容器 ID 和远程 session ID...(process.env.CLAUDE_CODE_CONTAINER_ID && { claudeCodeContainerId: process.env.CLAUDE_CODE_CONTAINER_ID,}),...(process.env.CLAUDE_CODE_REMOTE_SESSION_ID && { claudeCodeRemoteSessionId: process.env.CLAUDE_CODE_REMOTE_SESSION_ID,}),// 是否在 GitHub Actions 环境isGithubAction: isEnvTruthy(process.env.GITHUB_ACTIONS),// GitHub Actions 的详细元数据...(isEnvTruthy(process.env.GITHUB_ACTIONS) && { githubEventName: process.env.GITHUB_EVENT_NAME, githubActionsRunnerEnvironment: process.env.RUNNER_ENVIRONMENT, githubActionsRunnerOs: process.env.RUNNER_OS,}),
用户提示词默认会被记录,但内容被脱敏。来看 src/utils/telemetry/events.ts:
functionisUserPromptLoggingEnabled(): boolean{returnfalse// 默认关闭}exportfunctionredactIfDisabled(content: string): string{return isUserPromptLoggingEnabled() ? content : '<REDACTED>'}
Tool 的输入输出则由环境变量 OTEL_LOG_TOOL_DETAILS 控制:
exportfunctionisToolDetailsLoggingEnabled(): boolean{return isEnvTruthy(process.env.OTEL_LOG_TOOL_DETAILS)}
这远不是什么”基础使用数据分析”。这是一份相当详细的环境画像。
6. 内部构建版本采集更多
源码中有一个 USER_TYPE=ant 的内部模式(ant 应该是 Anthropic 的缩写),能采集更多信息。
来看 src/services/internalLogging.ts:
const getKubernetesNamespace = memoize(async () => {if (process.env.USER_TYPE !== 'ant') {returnnull// 非内部用户直接跳过 }const namespacePath ='/var/run/secrets/kubernetes.io/serviceaccount/namespace'const content = await readFile(namespacePath, { encoding: 'utf8' })return content.trim()})exportconst getContainerId = memoize(async () => {if (process.env.USER_TYPE !== 'ant') {returnnull }const containerIdPath = '/proc/self/mountinfo'// 匹配 Docker 和 containerd/CRI-O 容器 IDconst containerIdPattern =/(?:\/docker\/containers\/|\/sandboxes\/)([0-9a-f]{64})/// ... 从 mountinfo 中提取容器 ID})
所有这些都会被记录到一个内部遥测事件里:
exportasyncfunctionlogPermissionContextForAnts( toolPermissionContext, moment,) {if (process.env.USER_TYPE !== 'ant') returnvoid logEvent('tengu_internal_record_permission_context', {namespace: await getKubernetesNamespace(), toolPermissionContext: jsonStringify(toolPermissionContext), containerId: await getContainerId(), })}
这意味着在内部版本中,行为可以被追溯到一个非常具体的部署环境——哪个 Kubernetes 命名空间、哪个容器、什么权限上下文。
总结
把上面的发现串起来:
|
|
|
|---|---|
| 语言情绪 |
|
| UI 交互 |
|
| 反馈收集 |
|
| 隐藏命令 |
|
| 环境指纹 |
|
| 内部模式 |
|
这不只是一个聊天机器人。
这是一个高度插桩的系统,在观察你如何与它互动。
我不是说这里面有什么恶意。但当你读完源码,你会发现这个系统的可观测性和可度量性,远超大多数用户的预期。
大多数人永远不会看到这一层。
如果你在日常工作中用 Claude Code,了解引擎盖下在发生什么,总是值得的。
你怎么看?这是大规模产品的正常遥测,还是过度插桩?
如果你想一起研究 Claude Code 源码,欢迎加入我的社群,后台私信”社群”
夜雨聆风