# Local API Proxy -- example configuration. # # COMMIT THIS FILE. DO NOT COMMIT .env. # .gitignore excludes .env and every variant of it; this example is the only # one that belongs in the repository. Never paste a real key into this file. # # cp .env.example .env # # then edit .env and replace the two REPLACE-ME values below, # # and set ALLOWED_HOSTS if this is anything but a local Docker run # # Every variable here is REQUIRED and has no default anywhere in the code. # Leaving one out, or leaving it empty, stops the proxy at startup with a # message naming every missing variable at once. Values are trimmed, and an # empty value counts as missing. # # Do not quote values. Docker Compose treats quotes in an env_file as part of # the value, so PORT="18989" becomes the literal string with quotes and fails # the integer check. # --------------------------------------------------------------------------- # Compose only -- not read by proxy.mjs # --------------------------------------------------------------------------- # Port on YOUR machine that the proxy is published on, bound to 127.0.0.1 only. # Point Claude Code at this port. Change it if 18989 is already taken locally; # it does not have to match PORT. HOST_PORT=18989 # --------------------------------------------------------------------------- # Network # --------------------------------------------------------------------------- # Bind address inside the container. Docker Compose OVERRIDES this to 0.0.0.0, # because 127.0.0.1 in a container is the container's own loopback and would # make the published port unreachable. It is kept here only for running # `node proxy.mjs` directly outside Docker, where 127.0.0.1 is correct. # Format: an IP or hostname, no spaces. HOST=127.0.0.1 # Port the proxy listens on INSIDE the container. This is the only port that # carries /v1/* traffic, and the only one the reverse proxy ever routes to. # Format: integer 1-65535. Keep it at or above 1024 -- the container runs as a # non-root user and cannot bind privileged ports. PORT=18989 # Port the health endpoint listens on, INSIDE the container only. # # GET /health is served here and nowhere else. It is no longer answered on # PORT, it is not published in docker-compose.yml, and no reverse proxy route # points at it -- the listener is bound to the container's own loopback, so # only something already inside the container can reach it. That is what makes # it safe for the endpoint to stay unauthenticated: the Docker healthcheck runs # inside the container and can reach it, and nothing on the network can. # # Format: integer 1-65535, at or above 1024 for the same non-root reason as # PORT, and it MUST NOT equal PORT. Setting them the same stops startup. HEALTH_PORT=18990 # Hostnames this proxy will answer to. A request whose Host header is not on # this list is refused before anything is forwarded upstream. # # This is what stops DNS rebinding and drive-by cross-site requests. Without # it, a page you merely visit can point a hostname it controls at this proxy's # address and spend your gateway credential from your own browser; the Host # header is the one part of that attack the attacker cannot forge away. # # THE RULE: copy the host portion out of the ANTHROPIC_BASE_URL you give # Claude Code. If those two disagree every request fails, and this variable is # the reason. # # Entries are comma-separated, and each one may be a bare hostname, a # host:port, or a whole URL -- the scheme and the port are stripped and only # the hostname is compared. So the left column below is accepted and means # exactly the right column: # # https://claude-proxy..nip.io -> claude-proxy..nip.io # http://127.0.0.1:18989 -> 127.0.0.1 # http://localhost:18989 -> localhost # # The value shipped here is the local-Docker default and covers nothing else. # A deployment reached over a public hostname MUST replace it with that # hostname -- 127.0.0.1 and localhost are what the container sees itself as, # not what a remote client sends, so leaving this line alone on a VPS rejects # every real request. # Format: comma-separated list, unquoted. ALLOWED_HOSTS=127.0.0.1,localhost # --------------------------------------------------------------------------- # Upstream gateway # --------------------------------------------------------------------------- # Base URL of the Anthropic-compatible gateway every request is forwarded to. # Format: absolute URL, http:// or https:// only. A trailing path is allowed # and is prefixed onto every route. Any other scheme is rejected. UPSTREAM_BASE_URL=https://anyrouter.top # Gateway credential, sent upstream as "Authorization: Bearer ". # THIS IS A PLACEHOLDER AND WILL NOT WORK -- replace it with your own key. # Format: printable ASCII, no whitespace, at least 8 characters. # Treat the real value as a password: it bills to your account. UPSTREAM_API_KEY=sk-REPLACE-WITH-YOUR-GATEWAY-KEY # Shared secret Claude Code must present to THIS proxy. Every /v1/* request is # rejected with 401 unless it matches. # # This is the ONLY thing between the open internet and the paid # UPSTREAM_API_KEY above. Whoever guesses it spends your money. Earlier # versions of this file said a short placeholder was fine because the port was # bound to loopback; that stopped being true the moment this proxy was reached # over a public hostname, and the advice is withdrawn. # # Generate it, do not invent it: # # openssl rand -base64 32 # # Format: printable ASCII, no whitespace, MINIMUM 32 characters. The # placeholder below is deliberately shorter than that, so a .env left # unedited stops at startup instead of running with a key printed in a public # repository. LOCAL_PROXY_KEY=REPLACE-ME # --------------------------------------------------------------------------- # Client identity # --------------------------------------------------------------------------- # Version in the synthesized User-Agent: claude-cli/ (external, sdk-cli) # Used only when the calling client did not send its own User-Agent, which in # practice means curl and nothing else -- Claude Code always sends its own. # This is not the override User-Agent. The override strings are hardcoded # in proxy.mjs and are not set here. # Format: non-empty printable ASCII, no newlines. CLAUDE_CODE_VERSION=2.1.197 # When true, every upstream request replaces User-Agent with a hardcoded # first-party CLI identity for that path's dialect, including when the # caller sent one. The strings are not configurable. # Anthropic paths send: claude-cli/2.1.270 (external, cli) # OpenAI paths send: codex_cli_rs/0.154.0 (Linux 6.12.0; x86_64) unknown # When false, the current forwarding and bare-client defaults stay as they are. # Format: exactly "true" or "false". OVERRIDE_UPSTREAM_USER_AGENT=false # Model used when a request body omits "model". # Format: a model id string. UPSTREAM_MODEL=claude-opus-4-8 # --------------------------------------------------------------------------- # Limits and timeouts # --------------------------------------------------------------------------- # Socket timeout for each upstream request, in milliseconds. The server's own # request timeout is this value plus 30000. # Format: integer of at least 1. 300000 is 5 minutes. UPSTREAM_TIMEOUT_MS=300000 # Retry-After header value on 429 and 5xx responses, when the gateway did not # send its own. Format: integer of at least 1 (seconds). # Do not set this to 1 -- clients will hammer a throttled upstream. RETRY_AFTER_SECONDS=15 # Largest client request body accepted; anything bigger gets a 413. # Format: integer of at least 1048576 (1 MiB). 26214400 is 25 MiB. MAX_BODY_BYTES=26214400 # --------------------------------------------------------------------------- # Console logging # # All three flags accept EXACTLY "true" or "false". Nothing else is accepted: # not 1, not 0, not yes, not on, not empty. Anything else stops startup. # --------------------------------------------------------------------------- # Master switch for request logging to stdout. Under Docker this is everything # `docker compose logs` can show you, so false makes the container silent. PROXY_LOG=true # Adds full client and upstream header dumps plus request bodies to the log. # Leave false. When on, the upstream header dump includes the Authorization # header, and inbound client headers are dumped in full. Debugging only. PROXY_LOG_VERBOSE=false # Characters of a body or message written to the console before truncation. # Format: integer of at least 0. 0 truncates every logged body to nothing. # (Note this is the OPPOSITE of what 0 means for PROXY_TRACE_BODY_LIMIT.) PROXY_LOG_BODY_LIMIT=800 # --------------------------------------------------------------------------- # File tracing -- PRIVACY SENSITIVE, read this section # # The trace records full request bodies, full responses and raw SSE frames. # That means your complete prompts and the model's complete replies, in # plaintext, on disk. Redaction only masks the two keys above by exact match; # it does nothing for prompt content, and it does not catch a credential that # happens to appear inside a prompt or a reply. # # Shipped as false on purpose. Turn it on only while debugging. # --------------------------------------------------------------------------- # Where the trace is written. Under Docker this MUST be somewhere writable # inside the container: the root filesystem is read-only and /tmp is the only # writable path, mounted as a 64 MiB tmpfs that is discarded when the container # stops. A path anywhere else silently produces no trace at all. # Never point this inside the repository directory. # Format: a writable file path. PROXY_TRACE_FILE=/tmp/proxy-trace.log # Master switch for the file trace. Exactly "true" or "false". PROXY_TRACE=false # Characters of each traced value before truncation. # Format: integer of at least 0. 0 here means NO truncation -- entire bodies # are written verbatim. This is the opposite of PROXY_LOG_BODY_LIMIT=0, which # truncates to nothing. The two look alike and behave oppositely at 0. PROXY_TRACE_BODY_LIMIT=4000