{"slug": "agent-proxy-a-zero-dep-proxy-that-shows-the-bloat-in-claude-code-s-requests-tool", "title": "agent-proxy — a zero-dep proxy that shows the bloat in Claude Code's requests (ranked tool table + full readable Markdown of every request)", "summary": "A developer built agent-proxy, a zero-dependency logging proxy for Claude Code that sits between the CLI and the Anthropic API. The proxy forwards requests untouched and writes readable Markdown documents with a ranked table of what consumes context, revealing bloat in Claude Code's requests.", "body_md": "|\n/** |\n|\n* agent-proxy — see what Claude Code actually sends the model. |\n|\n* |\n|\n* A zero-dependency logging proxy for Claude Code. It sits between the CLI and |\n|\n* the Anthropic API, forwards every request untouched (auth header and all), |\n|\n* streams the response straight back so the CLI is unaffected, and for each |\n|\n* request writes a readable Markdown document — led by a ranked table of what |\n|\n* is eating your context. |\n|\n* |\n|\n* Run: node proxy.mjs |\n|\n* Point Claude Code at it: |\n|\n* ANTHROPIC_BASE_URL=http://localhost:8787 claude |\n|\n* |\n|\n* Zero runtime dependencies — Node built-ins only. Requires Node 18+. |\n|\n*/ |\n|\n|\n|\nimport http from \"node:http\"; |\n|\nimport https from \"node:https\"; |\n|\nimport fs from \"node:fs\"; |\n|\nimport path from \"node:path\"; |\n|\nimport { fileURLToPath } from \"node:url\"; |\n|\n|\n|\nconst PORT = Number(process.env.PORT ?? 8787); |\n|\nconst UPSTREAM = \"api.anthropic.com\"; |\n|\n|\n|\nconst HERE = path.dirname(fileURLToPath(import.meta.url)); |\n|\nconst LOG_DIR = path.join(HERE, \"logs\"); |\n|\n|\n|\n/** Rough token estimate for display. Real input tokens come from the response |\n|\n* usage; this is only for ranking the request before the reply arrives. */ |\n|\nconst estTokens = (bytes) => Math.round(bytes / 4); |\n|\n|\n|\n/** count_tokens calls send content but get back only a number, never a reply. |\n|\n* A single turn fires many as housekeeping — pure noise here, so skip them. */ |\n|\nconst isTokenCount = (reqPath) => reqPath.includes(\"count_tokens\"); |\n|\n|\n|\nconst REDACT = new Set([\"authorization\", \"x-api-key\", \"api-key\"]); |\n|\n|\n|\n/** Strip hop-by-hop and encoding headers so the captured response is readable, |\n|\n* recompute content-length, and pass auth through untouched so the real request |\n|\n* still authenticates. */ |\n|\nfunction forwardHeaders(headers, body) { |\n|\nconst out = { ...headers }; |\n|\ndelete out[\"host\"]; |\n|\ndelete out[\"connection\"]; |\n|\ndelete out[\"accept-encoding\"]; // force identity so we can read the stream |\n|\ndelete out[\"transfer-encoding\"]; |\n|\ndelete out[\"content-length\"]; |\n|\nif (body.length > 0) out[\"content-length\"] = String(body.length); |\n|\nreturn out; |\n|\n} |\n|\n|\n|\nfunction baseName() { |\n|\nconst stamp = new Date().toISOString().replace(/:/g, \"-\").replace(\".\", \"-\").replace(\"Z\", \"\"); |\n|\nreturn `${stamp}_anthropic`; |\n|\n} |\n|\n|\n|\n// --------------------------------------------------------------------------- |\n|\n// The audit: rank what's in the request |\n|\n// --------------------------------------------------------------------------- |\n|\n|\n|\n/** Measure every removable region of the request and rank the tools by size. |\n|\n* This is the whole point of the proxy — the numbers you cut against. */ |\n|\nfunction auditRequest(reqJson, realInputTokens) { |\n|\nconst tools = Array.isArray(reqJson?.tools) ? reqJson.tools : []; |\n|\nconst toolRows = tools |\n|\n.map((t) => { |\n|\nconst bytes = Buffer.byteLength(JSON.stringify(t)); |\n|\nreturn { name: t?.name ?? \"(unnamed)\", bytes, tokens: estTokens(bytes) }; |\n|\n}) |\n|\n.sort((a, b) => b.bytes - a.bytes); |\n|\n|\n|\nconst toolsBytes = toolRows.reduce((n, r) => n + r.bytes, 0); |\n|\nconst systemBytes = reqJson?.system ? Buffer.byteLength(JSON.stringify(reqJson.system)) : 0; |\n|\nconst totalBytes = Buffer.byteLength(JSON.stringify(reqJson ?? {})); |\n|\n|\n|\nreturn { |\n|\ntoolRows, |\n|\ntoolCount: toolRows.length, |\n|\ntoolsBytes, |\n|\nsystemBytes, |\n|\ntotalBytes, |\n|\nrealInputTokens, |\n|\n}; |\n|\n} |\n|\n|\n|\n/** The ranked table, as Markdown. The hero of the whole document. */ |\n|\nfunction renderAudit(a) { |\n|\nconst pct = (b) => (a.totalBytes ? ((b / a.totalBytes) * 100).toFixed(1) : \"0.0\"); |\n|\nconst rows = a.toolRows |\n|\n.map((r) => `| ${r.name} | ${r.bytes.toLocaleString()} | ~${r.tokens.toLocaleString()} | ${pct(r.bytes)}% |`) |\n|\n.join(\"\\n\"); |\n|\n|\n|\nreturn [ |\n|\n\"<audit>\", |\n|\n\"\", |\n|\na.realInputTokens != null |\n|\n? `**${a.realInputTokens.toLocaleString()} input tokens** billed for this request (from the response usage).` |\n|\n: \"\", |\n|\n\"\", |\n|\n`- **tools**: ${a.toolCount} definitions, ${a.toolsBytes.toLocaleString()} bytes (~${estTokens(a.toolsBytes).toLocaleString()} tokens)`, |\n|\n`- **system prompt**: ${a.systemBytes.toLocaleString()} bytes (~${estTokens(a.systemBytes).toLocaleString()} tokens)`, |\n|\n`- **total request**: ${a.totalBytes.toLocaleString()} bytes`, |\n|\n\"\", |\n|\n\"**Tools, ranked by size — this is your cut list:**\", |\n|\n\"\", |\n|\n\"| tool | bytes | ~tokens | % of request |\", |\n|\n\"| --- | --: | --: | --: |\", |\n|\nrows, |\n|\n\"\", |\n|\n\"</audit>\", |\n|\n].join(\"\\n\"); |\n|\n} |\n|\n|\n|\n/** The same ranking, compact, for the terminal — so you see the bloat live. */ |\n|\nfunction printAudit(a, base) { |\n|\nconst top = a.toolRows.slice(0, 12); |\n|\nconst w = Math.max(4, ...top.map((r) => r.name.length)); |\n|\nconsole.log(`\\n[agent-proxy] ${a.toolCount} tools · ${a.toolsBytes.toLocaleString()} tool bytes` + |\n|\n(a.realInputTokens != null ? ` · ${a.realInputTokens.toLocaleString()} real input tokens` : \"\")); |\n|\nfor (const r of top) { |\n|\nconsole.log(` ${r.name.padEnd(w)} ${String(r.bytes).padStart(7)} B ~${r.tokens} tok`); |\n|\n} |\n|\nif (a.toolRows.length > top.length) console.log(` … ${a.toolRows.length - top.length} more`); |\n|\nconsole.log(` logs/${base}.md\\n`); |\n|\n} |\n|\n|\n|\n// --------------------------------------------------------------------------- |\n|\n// Readable Markdown render (Anthropic /messages only) |\n|\n// --------------------------------------------------------------------------- |\n|\n|\n|\nconst fenceJson = (v) => \"``` json\\n\" + JSON.stringify(v, null, 2) + \"\\n```\"; |\n|\nconst fence = (t, lang = \"\") => \"```\" + lang + \"\\n\" + t + \"\\n```\"; |\n|\n|\n|\nfunction blockText(b) { |\n|\nif (typeof b === \"string\") return b; |\n|\nif (b?.type === \"text\" && typeof b.text === \"string\") return b.text; |\n|\nreturn \"\"; |\n|\n} |\n|\n|\n|\nfunction renderSystem(system) { |\n|\nif (typeof system === \"string\") return system; |\n|\nif (Array.isArray(system)) { |\n|\nreturn system |\n|\n.map((b) => blockText(b) + (b?.cache_control ? \"\\n\\n<!-- cache_control breakpoint -->\" : \"\")) |\n|\n.join(\"\\n\\n\"); |\n|\n} |\n|\nreturn fenceJson(system); |\n|\n} |\n|\n|\n|\nfunction renderTools(tools) { |\n|\nconst rendered = tools.map((t) => { |\n|\nconst lines = [`### ${t.name ?? \"(unnamed tool)\"}`, \"\"]; |\n|\nif (t.description) lines.push(t.description, \"\"); |\n|\nif (t.input_schema) lines.push(fenceJson(t.input_schema)); |\n|\nreturn lines.join(\"\\n\"); |\n|\n}); |\n|\nreturn [\"<tools>\", \"\", rendered.join(\"\\n\\n\"), \"\", \"</tools>\"].join(\"\\n\"); |\n|\n} |\n|\n|\n|\nfunction imagePlaceholder(b) { |\n|\nconst src = b.source ?? {}; |\n|\nconst bytes = typeof src.data === \"string\" ? src.data.length : 0; |\n|\nreturn `\\`[image: ${src.media_type ?? \"unknown\"}, ${bytes} base64 chars — full data in .request.txt]\\``; |\n|\n} |\n|\n|\n|\nfunction renderContent(content) { |\n|\nif (typeof content === \"string\") return content; |\n|\nif (!Array.isArray(content)) return fenceJson(content); |\n|\nreturn content |\n|\n.map((b) => { |\n|\nswitch (b?.type) { |\n|\ncase \"text\": |\n|\nreturn b.text ?? \"\"; |\n|\ncase \"tool_use\": |\n|\nreturn [`<tool-use name=\"${b.name}\" id=\"${b.id ?? \"\"}\">`, \"\", fenceJson(b.input ?? {}), \"\", \"</tool-use>\"].join(\"\\n\"); |\n|\ncase \"tool_result\": { |\n|\nconst inner = |\n|\ntypeof b.content === \"string\" |\n|\n? b.content |\n|\n: Array.isArray(b.content) |\n|\n? b.content.map((x) => (x?.type === \"image\" ? imagePlaceholder(x) : blockText(x) || fenceJson(x))).join(\"\\n\\n\") |\n|\n: fenceJson(b.content); |\n|\nreturn [`<tool-result tool-use-id=\"${b.tool_use_id ?? \"\"}\" is-error=\"${!!b.is_error}\">`, \"\", inner, \"\", \"</tool-result>\"].join(\"\\n\"); |\n|\n} |\n|\ncase \"image\": |\n|\nreturn imagePlaceholder(b); |\n|\ncase \"thinking\": |\n|\nreturn [\"<thinking>\", \"\", b.thinking ?? \"\", \"\", \"</thinking>\"].join(\"\\n\"); |\n|\ndefault: |\n|\nreturn fenceJson(b); |\n|\n} |\n|\n}) |\n|\n.join(\"\\n\\n\"); |\n|\n} |\n|\n|\n|\nfunction renderMessages(messages) { |\n|\nif (!Array.isArray(messages)) return \"<messages></messages>\"; |\n|\nconst rendered = messages.map((m, i) => |\n|\n[`<message index=\"${i + 1}\" role=\"${m.role ?? \"unknown\"}\">`, \"\", renderContent(m.content), \"\", \"</message>\"].join(\"\\n\") |\n|\n); |\n|\nreturn [\"<messages>\", \"\", rendered.join(\"\\n\\n\"), \"\", \"</messages>\"].join(\"\\n\"); |\n|\n} |\n|\n|\n|\n/** Reassemble the streamed SSE response so we can read the reply — and pull the |\n|\n* real input-token count out of the usage events. */ |\n|\nfunction decodeResponse(raw) { |\n|\nconst events = []; |\n|\nfor (const line of raw.split(/\\r?\\n/)) { |\n|\nconst m = line.match(/^data:\\s?(.*)$/); |\n|\nif (!m || m[1] === \"[DONE]\" || m[1].trim() === \"\") continue; |\n|\ntry { events.push(JSON.parse(m[1])); } catch { /* skip */ } |\n|\n} |\n|\nconst blocks = {}; |\n|\nlet stopReason, usage; |\n|\nfor (const ev of events) { |\n|\nif (ev.type === \"content_block_start\") blocks[ev.index] = { type: ev.content_block?.type ?? \"text\", text: \"\", name: ev.content_block?.name, id: ev.content_block?.id }; |\n|\nelse if (ev.type === \"content_block_delta\" && blocks[ev.index]) { |\n|\nconst d = ev.delta ?? {}; |\n|\nblocks[ev.index].text += d.text ?? d.partial_json ?? d.thinking ?? \"\"; |\n|\n} else if (ev.type === \"message_start\" && ev.message?.usage) usage = { ...ev.message.usage, ...(usage ?? {}) }; |\n|\nelse if (ev.type === \"message_delta\") { |\n|\nif (ev.delta?.stop_reason) stopReason = ev.delta.stop_reason; |\n|\nif (ev.usage) usage = { ...(usage ?? {}), ...ev.usage }; |\n|\n} |\n|\n} |\n|\nconst parts = []; |\n|\nif (stopReason) parts.push(`- **stop reason**: ${stopReason}`); |\n|\nif (usage) parts.push(`- **usage**: ${JSON.stringify(usage)}`, \"\"); |\n|\nfor (const i of Object.keys(blocks).map(Number).sort((a, b) => a - b)) { |\n|\nconst b = blocks[i]; |\n|\nif (b.type === \"text\") parts.push([\"<assistant-text>\", \"\", b.text, \"\", \"</assistant-text>\"].join(\"\\n\")); |\n|\nelse if (b.type === \"thinking\") parts.push([\"<thinking>\", \"\", b.text, \"\", \"</thinking>\"].join(\"\\n\")); |\n|\nelse if (b.type === \"tool_use\") parts.push([`<tool-use name=\"${b.name}\" id=\"${b.id ?? \"\"}\">`, \"\", fence(b.text || \"{}\", \"json\"), \"\", \"</tool-use>\"].join(\"\\n\")); |\n|\n} |\n|\nconst inputTokens = usage |\n|\n? (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) |\n|\n: null; |\n|\nreturn { markdown: parts.length ? parts.join(\"\\n\\n\") : fence(raw), inputTokens }; |\n|\n} |\n|\n|\n|\nfunction renderMarkdown(c, audit, responseMd) { |\n|\nconst headers = Object.entries(c.headers).map(([k, v]) => |\n|\n`${k}: ${REDACT.has(k.toLowerCase()) ? \"[REDACTED]\" : Array.isArray(v) ? v.join(\", \") : v ?? \"\"}` |\n|\n); |\n|\nconst req = c.reqJson; |\n|\nconst parts = [ |\n|\n[\"<meta>\", \"\", `- **timestamp**: ${c.timestamp}`, `- **model**: ${req?.model ?? \"unknown\"}`, `- **endpoint**: ${c.method} ${c.path}`, `- **upstream status**: ${c.statusCode}`, \"\", \"</meta>\"].join(\"\\n\"), |\n|\nrenderAudit(audit), |\n|\n[\"<headers>\", \"\", \"```\", ...headers, \"```\", \"\", \"</headers>\"].join(\"\\n\"), |\n|\n]; |\n|\nif (req?.system != null) parts.push([\"<system-prompt>\", \"\", renderSystem(req.system), \"\", \"</system-prompt>\"].join(\"\\n\")); |\n|\nif (Array.isArray(req?.tools) && req.tools.length) parts.push(renderTools(req.tools)); |\n|\nparts.push(renderMessages(req?.messages)); |\n|\nparts.push(\"<response>\\n\\n\" + responseMd + \"\\n\\n</response>\"); |\n|\nreturn parts.join(\"\\n\\n\") + \"\\n\"; |\n|\n} |\n|\n|\n|\n// --------------------------------------------------------------------------- |\n|\n// Server |\n|\n// --------------------------------------------------------------------------- |\n|\n|\n|\nfunction handle(req, res) { |\n|\nconst reqPath = req.url ?? \"/\"; |\n|\nconst chunks = []; |\n|\nreq.on(\"data\", (c) => chunks.push(c)); |\n|\nreq.on(\"end\", () => { |\n|\nconst body = Buffer.concat(chunks); |\n|\nconst timestamp = new Date().toISOString(); |\n|\nconst base = baseName(); |\n|\n|\n|\nconst upstream = https.request( |\n|\n{ hostname: UPSTREAM, port: 443, path: reqPath, method: req.method, headers: forwardHeaders(req.headers, body) }, |\n|\n(up) => { |\n|\nres.writeHead(up.statusCode ?? 502, up.headers); |\n|\nconst respChunks = []; |\n|\nup.on(\"data\", (c) => { respChunks.push(c); res.write(c); }); |\n|\nup.on(\"end\", () => { |\n|\nres.end(); |\n|\nif (isTokenCount(reqPath)) return; |\n|\ntry { |\n|\nconst reqJson = JSON.parse(body.toString(\"utf8\")); |\n|\nconst { markdown, inputTokens } = decodeResponse(Buffer.concat(respChunks).toString(\"utf8\")); |\n|\nconst audit = auditRequest(reqJson, inputTokens); |\n|\nfs.mkdirSync(LOG_DIR, { recursive: true }); |\n|\nfs.writeFileSync(path.join(LOG_DIR, `${base}.request.txt`), body.toString(\"utf8\")); |\n|\nfs.writeFileSync(path.join(LOG_DIR, `${base}.md`), renderMarkdown({ reqJson, timestamp, method: req.method ?? \"POST\", path: reqPath, statusCode: up.statusCode ?? 0, headers: req.headers }, audit, markdown)); |\n|\nprintAudit(audit, base); |\n|\n} catch (err) { |\n|\nconsole.error(`[agent-proxy] could not render (non-JSON body?): ${err.message}`); |\n|\n} |\n|\n}); |\n|\n} |\n|\n); |\n|\nupstream.on(\"error\", (err) => { |\n|\nconsole.error(`[agent-proxy] upstream error: ${err.message}`); |\n|\nif (!res.headersSent) res.writeHead(502, { \"content-type\": \"application/json\" }); |\n|\nres.end(JSON.stringify({ error: `agent-proxy upstream error: ${err.message}` })); |\n|\n}); |\n|\nif (body.length > 0) upstream.write(body); |\n|\nupstream.end(); |\n|\n}); |\n|\n} |\n|\n|\n|\nhttp.createServer(handle).listen(PORT, () => { |\n|\nconsole.log(`[agent-proxy] listening on http://localhost:${PORT}`); |\n|\nconsole.log(`[agent-proxy] point Claude Code at it: ANTHROPIC_BASE_URL=http://localhost:${PORT} claude`); |\n|\n}); |", "url": "https://wpnews.pro/news/agent-proxy-a-zero-dep-proxy-that-shows-the-bloat-in-claude-code-s-requests-tool", "canonical_source": "https://gist.github.com/mattpocock/5b3d76ea21f5f698aefded47a9cea3b1", "published_at": "2026-07-07 11:51:08+00:00", "updated_at": "2026-07-07 13:28:15.757142+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Claude Code", "Anthropic", "agent-proxy"], "alternates": {"html": "https://wpnews.pro/news/agent-proxy-a-zero-dep-proxy-that-shows-the-bloat-in-claude-code-s-requests-tool", "markdown": "https://wpnews.pro/news/agent-proxy-a-zero-dep-proxy-that-shows-the-bloat-in-claude-code-s-requests-tool.md", "text": "https://wpnews.pro/news/agent-proxy-a-zero-dep-proxy-that-shows-the-bloat-in-claude-code-s-requests-tool.txt", "jsonld": "https://wpnews.pro/news/agent-proxy-a-zero-dep-proxy-that-shows-the-bloat-in-claude-code-s-requests-tool.jsonld"}}