#!/usr/bin/env python3 """Logging HTTP origin proxy for Claude Code. LOCAL test aid only. Claude is pointed at this proxy via ANTHROPIC_BASE_URL; every request is logged and forwarded to the real Anthropic backend untouched. Optionally the remote-settings endpoint is answered with a local mock payload (--mock) — note a custom base URL makes real remote settings ineligible, so the mock is mainly for manual curl testing, not the live settings fetch. Usage: python3 proxy.py # listen 127.0.0.1:8787 python3 proxy.py --mock --port 8788 # answer settings GET with mock python3 proxy.py --upstream http://localhost:9000 Then: ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude -p ... """ import argparse, hashlib, json, re, time from urllib.parse import urlparse import http.client from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer LOG = lambda *a: print(f"[{time.strftime('%H:%M:%S')}]", *a, flush=True) MOCK_SETTINGS = { "$schema": "https://json.schemastore.org/claude-code-settings.json", "companyAnnouncements": [ "PROXY-INJECTED announcement - this came from your local passthrough proxy, not Anthropic." ], "permissions": { "allow": ["Bash(echo *)", "Read(./**)"], "deny": [], "ask": [], "disableBypassPermissionsMode": "off" } } HOP_HEADERS = {"connect", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host", "proxy-connection"} def route(server, method: str, path: str, headers, body: bytes, wfile): upstream = server.upstream LOG(f"{method} {path} Host={headers.get('Host', '?')} -> {upstream}") # Mock the settings endpoint. if server.mock and method == "GET" and re.search(r"/api/claude_code/settings\b", path): payload = server.mock_body wfile.write(b"HTTP/1.1 200 OK\r\n") wfile.write(b"Content-Type: application/json\r\n") wfile.write(f'ETag: "{hashlib.sha256(payload).hexdigest()}"\r\n'.encode()) wfile.write(f"Content-Length: {len(payload)}\r\n".encode()) wfile.write(b"\r\n") wfile.write(payload) wfile.flush() LOG(f" -> MOCK 200 (settings) len={len(payload)}") return # Forward to upstream. u = urlparse(upstream) conn = (http.client.HTTPSConnection(u.netloc, timeout=30) if u.scheme == "https" else http.client.HTTPConnection(u.netloc, timeout=30)) hs = {k: v for k, v in headers.items() if k.lower() not in HOP_HEADERS} try: conn.request(method, path, body=body or None, headers=hs) r = conn.getresponse() rbody = r.read() LOG(f" -> {r.status} {r.reason} len={len(rbody)}") wfile.write(f"HTTP/1.1 {r.status} {r.reason}\r\n".encode()) for k, v in r.getheaders(): if k.lower() not in HOP_HEADERS: wfile.write(f"{k}: {v}\r\n".encode()) # Delimit the body ourselves so the client never hangs on keep-alive. wfile.write(f"Content-Length: {len(rbody)}\r\n".encode()) wfile.write(b"Connection: close\r\n\r\n") wfile.write(rbody) wfile.flush() except Exception as e: LOG(f" !! forward error: {e}") wfile.write(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n") wfile.flush() finally: conn.close() class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def log_message(self, fmt, *args): pass def do_GET(self): route(self.server, "GET", self.path, self.headers, None, self.wfile) def do_HEAD(self): route(self.server, "HEAD", self.path, self.headers, None, self.wfile) def do_POST(self): route(self.server, "POST", self.path, self.headers, self._read_body(), self.wfile) def _read_body(self): cl = int(self.headers.get("Content-Length") or 0) return self.rfile.read(cl) if cl else b"" def main(): ap = argparse.ArgumentParser() ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", type=int, default=8787) ap.add_argument("--upstream", default="https://api.anthropic.com") ap.add_argument("--mock", action="store_true", help="answer GET /api/claude_code/settings with a mock payload") args = ap.parse_args() srv = ThreadingHTTPServer((args.host, args.port), Handler) srv.upstream = args.upstream srv.mock = args.mock srv.mock_body = json.dumps(MOCK_SETTINGS).encode() LOG(f"listening on http://{args.host}:{args.port} upstream={args.upstream} mock={args.mock}") try: srv.serve_forever() except KeyboardInterrupt: print("\nbye") srv.server_close() if __name__ == "__main__": main()