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
This commit is contained in:
Claude
2026-04-16 17:58:48 +00:00
parent 16c8275f4e
commit 331b1cb2a2
3 changed files with 86 additions and 29 deletions
+86 -9
View File
@@ -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);
}
}
}
}
-11
View File
@@ -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;
-9
View File
@@ -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 || [];