Skip to main content

argo-shim

A lightweight HTTP proxy that lets Claude Code talk to the Argo API through an SSH tunnel from an ALCF machine. It handles path rewriting (/v1/messages -> /argoapi/v1/messages), injects your API key, and bridges plain HTTP (what Claude Code speaks) to HTTPS (what the tunnel carries).

Installation

# Run directly (no install needed):
uvx argo-shim

# Or install globally:
pip install argo-shim
# then run:
argo-shim

Prerequisites

  • SSH access to CELS machines (setup guide)
  • Python 3.8+
  • Claude Code (curl -fsSL https://claude.ai/install.sh | bash)

First-time setup (new users start here)

argo-shim reaches the Argo API over an SSH tunnel to CELS. That tunnel only works if CELS recognizes your SSH key. Set this up once, and verify it works, before you run argo-shim. Skipping this is the #1 cause of failures.

  1. Generate an SSH key (press Enter at every prompt to accept defaults):
    ssh-keygen -t ed25519
    
  2. Upload your public key to your CELS account. Print it and copy the whole line:
    cat ~/.ssh/id_ed25519.pub
    
    Paste it into the SSH Keys section at https://accounts.cels.anl.gov. Paste the .pub contents — never your private key.
  3. Load the key into your SSH agent:
    ssh-add
    
  4. Verify it works. This must log you in without a password prompt:
    ssh -o BatchMode=yes -J logins.cels.anl.gov homes.cels.anl.gov true
    
    If that command succeeds (no output, no password prompt, exit 0), you're ready. If it fails, fix it here — argo-shim cannot work until this does.

argo-shim runs this same check for you on startup and refuses to start if it finds no SSH key at all, pointing you back to these steps. That's on purpose — see the warning below.

⚠️ If argo-shim fails, do not keep restarting it. ALCF login nodes are shared, and CELS/CSPO security blocks the whole node's IP after too many failed SSH logins — which would break Argo access for everyone on that node (and is why this is getting locked down). A failed connection almost always means a setup problem that a restart won't fix. Read the error message, fix the one thing it names, then try again. argo-shim now enforces a cooldown after repeated failures so an accidental restart loop can't get the node blocked (see SSH Auth Failure Protection).

Quick Start

1. Run the shim

argo-shim

The shim will:

  • Find or create an SSH tunnel to apps.inside.anl.gov:443
  • Start a local HTTP proxy on a port derived from your username (deterministic across restarts)
  • Generate a per-session auth token and update ~/.claude/settings.json with the correct ANTHROPIC_BASE_URL and apiKeyHelper
  • Run health checks to verify connectivity

To use a specific port instead of the auto-derived one:

argo-shim --port 8083

The tunnel will use the port immediately below (e.g., --port 8083 → tunnel on 8082, shim on 8083).

If your ALCF username differs from your CELS username, set CELS_USERNAME to your CELS username

2. Start Claude Code (in another terminal on the same node)

claude

Already running? Restarting safely

Rerunning argo-shim is safe. If a healthy shim of yours is already running, a plain argo-shim detects it, re-syncs ~/.claude/settings.json, and exits without touching SSH — so an accidental second launch never triggers another Duo login. (This matters on shared login nodes: repeated SSH auths are what get a node's IP blocked.)

You run argo-shim and… What happens
a healthy shim is already running "already running — nothing to do", settings re-synced, exits. No SSH.
a shim is running but its tunnel died Tells you it will self-recover on the next request; suggests --restart. No SSH, nothing killed.
the port is held by another user Friendly message to pick --port <PORT>. No SSH attempted.
nothing is running Normal startup (reuses a live tunnel if present, else creates one).

To force a clean restart of your own shim:

argo-shim --restart

--restart stops your existing shim and starts fresh. It reuses a still-healthy SSH tunnel (no new Duo prompt) and only rebuilds the tunnel if it's actually dead.

To check or clear SSH-failure state:

argo-shim --status   # show lockout/cooldown state (read-only)
argo-shim --reset    # clear the lockout after fixing your SSH auth

Running from Compute Nodes

Compute nodes don't have outbound network access, so they can't create SSH tunnels directly. Instead, create the tunnel on a UAN and point the shim at it.

1. On a UAN:

argo-shim --tunnel

This creates an SSH tunnel bound to all interfaces and prints the command to run on the compute node.

2. On the compute node:

argo-shim --tunnel-host <uan-hostname>

Then start Claude Code. If proxy env vars are set (common on HPC nodes), bypass them for localhost:

no_proxy=localhost,127.0.0.1 NO_PROXY=localhost,127.0.0.1 claude

The shim prints this command automatically when it detects proxy variables. Without no_proxy, the proxy intercepts API traffic to 127.0.0.1 and breaks the connection.

Fallback: Relay through your Mac

If your UAN cannot SSH to CELS (e.g., network restrictions on Aurora), you can relay the tunnel through your Mac instead. This requires keeping your Mac connected for the duration of the session.

1. On your Mac:

argo-shim --relay <uan-hostname>

This creates the SSH tunnel locally, reverse-forwards it to the UAN, and starts the local shim (so your Mac can also use Claude Code).

2. On the compute node:

argo-shim --tunnel-host <uan-hostname>

If the UAN has GatewayPorts disabled (the default), the shim will automatically create an SSH local forward from the compute node to the UAN's localhost port.

Note: The relay approach adds an extra network hop (compute node -> UAN -> Mac -> CELS -> API) and depends on your Mac staying connected. Prefer --tunnel on the UAN when SSH to CELS is available.

Claude Code Settings

The shim automatically creates ~/.claude/settings.json on first run and keeps the port in ANTHROPIC_BASE_URL correct on subsequent runs. No manual setup needed.

To use a specific model (e.g., Opus), add a "model" field to your settings:

{
  "model": "claudeopus46"
}

Without this, Claude Code defaults to Sonnet.

OpenAI-compatible clients

The shim isn't just for Claude Code — any OpenAI- or Anthropic-format client can use it:

[!IMPORTANT] Authentication. In addition to x-api-key, the shim accepts its token via Authorization: Bearer <token>, so OpenAI SDKs / clients that only send a bearer token work without modification. This token is the shim's own access token (not your ALCF/CELS credential) — a random string the shim generates on first start and stores in ~/.claude/settings.json as the value after echo in apiKeyHelper. It's reused across restarts by default (pass --rotate-token to force a fresh one). Capture the current one into $token, which the examples below use:

token=$(python3 -c "import json, os; print(json.load(open(os.path.expanduser('~/.claude/settings.json')))['apiKeyHelper'].split()[-1])")
  • OpenAI & Gemini models. Argo serves these on /argoapi/v1/chat/completions and requires a user field set to a valid ALCF username, or it returns HTTP 500. The shim auto-injects this for any /chat/completions request that doesn't already set user. The value is resolved from $ARGO_USER, then $CELS_USERNAME, then your login username — set ARGO_USER if your ALCF username differs.

  • Model names. Argo expects its own model ids (e.g. gpt4o, GPT-4o), not the canonical OpenAI/Anthropic/Gemini spellings, and returns HTTP 400 Invalid model otherwise. The shim normalizes /chat/completions model names to Argo's ids, so canonical names like gpt-4o, gpt-4.1, claude-opus-4-8, or gemini-2.5-pro work unchanged. Names it doesn't recognize are forwarded as-is. List the exact ids with:

    curl -H "Authorization: Bearer ${token}" http://127.0.0.1:<shim-port>/v1/models
    

Putting it together — a complete request:

# OpenAI-format request via bearer auth. The shim auto-injects the user field
# and normalizes the model name (gpt-4o -> Argo's gpt4o).
curl http://127.0.0.1:<shim-port>/argoapi/v1/chat/completions \
     -H "Authorization: Bearer ${token}" \
     -H "content-type: application/json" \
     -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]}'

Using Codex CLI with argo-shim

Codex CLI only speaks the OpenAI Responses API, but argo-shim (like Argo itself) only implements Chat Completions. A small translation gateway, llm-rosetta, bridges the two:

Codex --(Responses API)--> llm-rosetta gateway --(Chat Completions)--> argo-shim --> Argo

1. Start argo-shim as usual (see Quick Start) and note its port from the startup output (✅ Shim running on <port> -> ...).

2. Install and configure llm-rosetta (requires Python 3.8+; >=0.7.3 to avoid a known upstream bug with null usage fields in Argo's responses):

pip install --user 'llm-rosetta>=0.7.3'

~/.config/llm-rosetta-gateway/config.jsonc:

{
  "providers": {
    "openai_chat": {
      "api_key": "placeholder",
      "base_url": "http://127.0.0.1:<shim-port>/v1"
    }
  },
  "models": {
    // Argo model ids you want exposed to Codex — see argo-shim's /v1/models
    "gpt54": "openai_chat",
    "gpt56sol": "openai_chat"
  },
  "server": {
    "host": "127.0.0.1",
    "port": 8765,
    // Required since llm-rosetta 0.7.3, or the gateway 403s every request.
    "api_key": "<any string — this is what Codex will send back to the gateway>"
  }
}

The "placeholder" value is fine — as long as base_url points at argo-shim's port, argo-shim keeps that provider's api_key in sync with its current auth token automatically on every start/restart, no manual copy-paste needed. The token itself is reused across restarts by default, so this is normally a no-op after the first sync. If you pass --rotate-token, restart the gateway afterward too — it reads config.jsonc once at its own startup and won't pick up the new token otherwise. This only applies to providers.*.api_key; server.api_key is a separate, gateway-owned secret you set yourself.

Restart argo-shim once to pick up the new config, then start the gateway:

argo-shim --restart
python3 -m llm_rosetta.gateway

3. Configure Codex (~/.codex/config.toml) to point at the gateway, not at argo-shim directly:

model = "gpt54"
model_provider = "argo"

[model_providers.argo]
name = "Argo via llm-rosetta gateway"
base_url = "http://127.0.0.1:8765/v1"
wire_api = "responses"
env_key = "ARGO_GATEWAY_KEY"
export ARGO_GATEWAY_KEY="<the server.api_key value from step 2>"
codex

Pointing Codex's base_url straight at argo-shim (skipping the gateway) will fail with error sending request for url (.../responses) — argo-shim doesn't implement the Responses API, only Chat Completions and Anthropic Messages.

Health Checks

The shim runs these automatically on startup. To run them manually:

# Tunnel only (direct HTTPS through tunnel; use your tunnel port from startup logs)
curl -k -H "Host: apps.inside.anl.gov" \
     -H "x-api-key: <username>" \
     https://127.0.0.1:<tunnel-port>/argoapi/v1/models

# Tunnel + shim end-to-end (use your shim port; token is printed at startup)
curl -H "x-api-key: <auth-token>" http://127.0.0.1:<shim-port>/v1/models

SSH Auth Failure Protection

ALCF and CELS networks are monitored by CSPO (Cyber Security). Too many failed SSH authentication attempts from a single IP will cause CSPO to block that IP — and since multiple users share ALCF login nodes, one user's broken auth can block the entire node for everyone.

argo-shim protects against this in three layers:

1. Preflight checks (before any SSH attempt). On startup, argo-shim looks for usable SSH key material. If you have no key at all, it prints the first-time setup steps and refuses to start — it will not make a doomed connection that counts against the node's IP. If you have a key on disk but nothing loaded in your agent, it warns you to run ssh-add (a common symptom of a laptop that slept and dropped agent forwarding).

2. Failure classification. When an SSH command does fail, argo-shim reads ssh's own error output and classifies it: an auth failure (Permission denied (publickey), etc.) is the dangerous, IP-block-triggering kind and is counted; a transient network error or a busy local port is not counted, because it isn't a failed login. Each failure prints a specific, actionable hint.

3. A persistent lockout that survives restarts. This is the key change. The failure counter is stored on disk (~/.claude/argo-shim-state.json), so quitting and re-running argo-shim does not reset it — you can no longer accidentally hammer CELS with a restart loop.

  • After 2 consecutive auth failures, all SSH attempts pause for a 15-minute cooldown. The cooldown clears itself.
  • If failures continue across multiple cooldowns, argo-shim escalates to a hard lock that only argo-shim --reset clears (after you've fixed auth).

Check or clear the state at any time:

argo-shim --status   # show current lockout/cooldown state (read-only)
argo-shim --reset    # clear the lockout after you've fixed your SSH auth

Common causes of repeated SSH auth failures:

  • SSH public key not uploaded to https://accounts.cels.anl.gov
  • Closing your laptop while SSH agent forwarding is active (kills the forwarded key)
  • SSH key removed from the agent (ssh-add -D) — fix with ssh-add
  • Expired Kerberos tickets

To recover from a cooldown or hard lock: first make this succeed — ssh -o BatchMode=yes -J logins.cels.anl.gov homes.cels.anl.gov true — then run argo-shim --reset (if hard-locked) and start argo-shim again.

All SSH commands also use BatchMode=yes (no interactive password fallback) and ConnectionAttempts=1 to ensure each attempt is a single, non-interactive connection.

Troubleshooting

Before anything else: confirm SSH itself works, independent of argo-shim:

ssh -o BatchMode=yes -J logins.cels.anl.gov homes.cels.anl.gov true

If that fails, argo-shim cannot work — fix SSH first (see the table below). Do not loop on restarting argo-shim while SSH is broken.

Symptom Likely cause Fix (do this one thing)
Permission denied (publickey) Public key not on your CELS account, or wrong key in use Upload ~/.ssh/id_ed25519.pub at https://accounts.cels.anl.gov, then ssh-add
argo-shim prints "No SSH key found" and exits You haven't set up an SSH key yet Follow First-time setup
Worked earlier, now Permission denied after closing your laptop Agent forwarding dropped when the laptop slept Reconnect, then ssh-add
"SSH attempts are paused for ~N minutes" You hit the failure cooldown Fix your SSH auth and wait out the cooldown; check with argo-shim --status
"SSH attempts are HARD-LOCKED" Repeated failures across cooldowns Fix SSH auth, then argo-shim --reset
Host key verification failed / host identity changed Stale entry in ~/.ssh/known_hosts Remove the stale line only if you trust the change, then retry once
Could not resolve hostname / Connection timed out Network/VPN problem, not auth Check your connection/VPN (this is not counted against the lockout)
"already running and healthy — nothing to do" You already have a shim running Nothing needed; use argo-shim --restart only if you want a fresh start
"already running … but its SSH tunnel is down" Shim is up, tunnel dropped Let it self-recover on the next request, or argo-shim --restart to rebuild now
"Port … is held by another process / NOT your argo-shim" Another user holds your derived port argo-shim --port <PORT> (no SSH was attempted)

[SSL: WRONG_VERSION_NUMBER] proxy errors

The SSH tunnel is stale, usually caused by SSH ControlMaster keeping a dead connection open. Fix:

ssh -O exit homes.cels.anl.gov
argo-shim   # re-creates the tunnel

HEAD /argoapi HTTP/1.1 501

You're running an older version of the shim that didn't handle HEAD requests. Update to the latest version.

Port already in use

The shim derives a deterministic port from your username. If that port is taken, specify a different one:

argo-shim --port 8083

To find and kill stale SSH tunnels occupying ports:

# List SSH tunnels
ps aux | grep 'ssh -N'
# Kill a specific one
kill <pid>

Claude Code can't connect / API connection refused

A few things to check:

  • Restart Claude Code after restarting the shim — Claude Code only reads ~/.claude/settings.json at startup, so it won't pick up a new port or token until restarted.
  • Try a different port — in rare cases the derived port may not work on your node. Use --port <PORT> to specify an alternative (e.g., argo-shim --port 8083).

401 errors / auth failures with project-level Claude settings

The shim writes apiKeyHelper and ANTHROPIC_BASE_URL to ~/.claude/settings.json (global). If you have a project-level .claude/settings.json with its own env object, it overrides the global env entirely (Claude Code does not merge object/scalar settings across scopes — only arrays merge). This means the shim's auth token never reaches Claude Code.

Fix: run the shim with --no-auth to disable token authentication:

argo-shim --no-auth

This is safe because the shim only listens on 127.0.0.1. You will still need ANTHROPIC_BASE_URL set correctly — either in your global settings (where the shim writes it) or in your project settings.

500: Streaming is required for operations that may take longer than 10 minutes

This error comes from Google Vertex AI (the backend hosting Claude behind Argo), not from the shim or Argo Gateway. It occurs when a non-streaming request (stream: false or omitted) has a large payload — typically when Claude Code sends tool results (file reads, web searches) back to the model.

The shim works around this by forcing stream: true on all POST requests to /messages before forwarding upstream. If you see this error, make sure you're running the latest version of the shim.

"ERROR: The requested URL could not be retrieved" in Claude Code

HPC login nodes set HTTP_PROXY / HTTPS_PROXY environment variables that can interfere with the shim's localhost proxy. The shim handles this automatically by setting no_proxy=localhost,127.0.0.1 in Claude Code's settings, so API traffic bypasses the proxy while internet access still works.

If you still see proxy errors, ensure you're running the latest version of the shim. See the ALCF proxy docs for more details.

Publishing to PyPI

Publishing is automated via GitHub Actions. To release a new version:

  1. Bump the version in argo_shim/__init__.py
  2. Commit and tag:
    git commit -am "Bump version to X.Y.Z"
    git tag vX.Y.Z
    git push && git push --tags
    

The workflow builds with uv build and publishes with uv publish using trusted publishing (no API tokens needed — configure the GitHub repo as a trusted publisher on PyPI).

Release files for argo-shim 0.3.20

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for argo-shim 0.3.20
File Size Uploaded
argo_shim-0.3.20.tar.gz 47.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for argo-shim 0.3.20
File Interpreter ABI Platform
argo_shim-0.3.20-py3-none-any.whl Python 3 none any Details

Total release size: 83.6 kB

Release files / argo_shim-0.3.20.tar.gz

Download URL argo_shim-0.3.20.tar.gz
Size 47.4 kB
Tags Source
SHA-256 checksum
How to use checksums
16eca271cad8f55ff6339edba10ce433d0a84cdf32d0265be2de574cf59d1242
BLAKE2b-256 checksum
How to use checksums
2c8ee6d67d9c4af6e796c34730a515490007c37af7005eaa3931ddc570cfbd58
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / argo_shim-0.3.20-py3-none-any.whl

Download URL argo_shim-0.3.20-py3-none-any.whl
Size 36.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
75f73275c9d2072efe183db98afbd28dbb993a60713efc35d35de664328ab64f
BLAKE2b-256 checksum
How to use checksums
578fabe92dbb8635e4a1ee85518c3dbc44624340c74a0c27b8f2cdf6eef78e45
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.3.25

2 release files

0.3.24

2 release files

0.3.23

2 release files

0.3.22

2 release files

0.3.21

2 release files

This release

0.3.20 This release

2 release files

0.3.18

2 release files

0.3.17

2 release files

0.3.16

2 release files

0.3.15

2 release files

0.3.14

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page