From 331b1cb2a26228314c0bef12519eab12d5e2243a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 17:58:48 +0000 Subject: [PATCH] Narrow claude.ai/code interception to user-input fields Replaces the blanket skip on claude.ai/code with a targeted walker so the user's prompt and pasted attachments still get substituted, but tool_use / tool_result / system / IDs / metadata pass through verbatim. Fields touched: - top-level prompt and input (classic claude.ai shape) - attachments[].extracted_content and attachments[].file_name - messages[].content (or text parts) when role === 'user' FormData uploads continue to flow through DocumentScanner since they are user input. Raw non-JSON bodies are skipped on this path because they are typically tool traffic, not user text. https://claude.ai/code/session_01Y2YprLMx348eWD5C9Z4zpV --- src/content/content.js | 95 +++++++++++++++++++++++++++++++++++---- src/content/early-hook.js | 11 ----- src/content/injector.js | 9 ---- 3 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/content/content.js b/src/content/content.js index c14dc1c..67b808a 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -754,6 +754,74 @@ return { modified, replacements: allReplacements }; } + // ============================================================ + // Claude Code (claude.ai/code) — narrow walker + // + // The web Claude Code app streams tool-call traffic (file paths, shell + // commands, GitHub URLs) through the same API as user messages. Deep-walking + // every string corrupts that traffic and makes tools fail. Instead we only + // substitute at paths that carry user-typed input: + // - top-level `prompt` (classic claude.ai chat field) + // - `attachments[].extracted_content` and `.file_name` (pasted content) + // - `messages[].content` / content parts where role === 'user' + // Anything outside these paths — tool_use, tool_result, system, tools, + // metadata, IDs — passes through untouched. + // ============================================================ + function isClaudeCode() { + return location.hostname === 'claude.ai' && /^\/code(\/|$)/.test(location.pathname); + } + + function processBodyClaudeCode(body) { + let modified = false; + const allReplacements = []; + + function processString(s) { + if (typeof s !== 'string' || s.length < MIN_STRING_LENGTH) return s; + const r = substituteAll(s); + if (r.modified) { + allReplacements.push(...r.replacements); + modified = true; + return r.text; + } + return s; + } + + if (body && typeof body === 'object') { + if (typeof body.prompt === 'string') body.prompt = processString(body.prompt); + if (typeof body.input === 'string') body.input = processString(body.input); + + if (Array.isArray(body.attachments)) { + for (const att of body.attachments) { + if (!att || typeof att !== 'object') continue; + if (typeof att.extracted_content === 'string') { + att.extracted_content = processString(att.extracted_content); + } + if (typeof att.file_name === 'string') { + att.file_name = processString(att.file_name); + } + } + } + + if (Array.isArray(body.messages)) { + for (const msg of body.messages) { + if (!msg || msg.role !== 'user') continue; + if (typeof msg.content === 'string') { + msg.content = processString(msg.content); + } else if (Array.isArray(msg.content)) { + for (const part of msg.content) { + // Only text parts — never tool_use / tool_result / image payloads + if (part?.type === 'text' && typeof part.text === 'string') { + part.text = processString(part.text); + } + } + } + } + } + } + + return { modified, replacements: allReplacements }; + } + // ============================================================ // Check if we have anything to substitute // ============================================================ @@ -879,7 +947,9 @@ try { // Try JSON const body = JSON.parse(options.body); - const { modified, replacements } = processBody(body); + const { modified, replacements } = isClaudeCode() + ? processBodyClaudeCode(body) + : processBody(body); if (modified) { options = { ...options, body: JSON.stringify(body) }; @@ -889,8 +959,10 @@ ); } } catch (e) { - // Not JSON — try raw string substitution (form data, etc.) - if (options.body.length > MIN_STRING_LENGTH) { + // Not JSON — try raw string substitution (form data, etc.). + // Skip on claude.ai/code: raw bodies there are typically tool + // traffic, not user input. + if (options.body.length > MIN_STRING_LENGTH && !isClaudeCode()) { const result = substituteAll(options.body); if (result.modified) { options = { ...options, body: result.text }; @@ -944,17 +1016,22 @@ ) { try { const parsed = JSON.parse(body); - const { modified, replacements } = processBody(parsed); + const { modified, replacements } = isClaudeCode() + ? processBodyClaudeCode(parsed) + : processBody(parsed); if (modified) { body = JSON.stringify(parsed); notifySubstitutions(replacements); } } catch (e) { - // Not JSON — raw string - const result = substituteAll(body); - if (result.modified) { - body = result.text; - notifySubstitutions(result.replacements); + // Not JSON — raw string. Skip on claude.ai/code; raw bodies there + // are typically tool traffic, not user input. + if (!isClaudeCode()) { + const result = substituteAll(body); + if (result.modified) { + body = result.text; + notifySubstitutions(result.replacements); + } } } } diff --git a/src/content/early-hook.js b/src/content/early-hook.js index b921422..d68dafe 100644 --- a/src/content/early-hook.js +++ b/src/content/early-hook.js @@ -9,17 +9,6 @@ * because sites like claude.ai have strict CSP that blocks inline scripts. */ (function () { - // Claude Code (web) runs at claude.ai/code and makes tool-call requests - // containing real file paths, usernames, and commands. Substituting any of - // these corrupts tool execution, so we never hook fetch/XHR on that app. - if ( - location.hostname === 'claude.ai' && - /^\/code(\/|$)/.test(location.pathname) - ) { - window.__ssSkipHost = true; - return; - } - window.__ssOriginalFetch = window.fetch; window.__ssOriginalXHROpen = XMLHttpRequest.prototype.open; window.__ssOriginalXHRSend = XMLHttpRequest.prototype.send; diff --git a/src/content/injector.js b/src/content/injector.js index 262384a..e609d65 100644 --- a/src/content/injector.js +++ b/src/content/injector.js @@ -15,15 +15,6 @@ if (window.__silentSendInjected) return; window.__silentSendInjected = true; - // Skip Claude Code (web). Its tool-call traffic carries real paths/commands - // that must reach the runtime verbatim — substitution breaks execution. - if ( - location.hostname === 'claude.ai' && - /^\/code(\/|$)/.test(location.pathname) - ) { - return; - } - // Merge active profiles into flat identity object function mergeProfiles(data) { const profiles = data?.profiles || [];