Route local AI tools (Claude Code, Codex, OpenCode, anything that talks to the Anthropic or OpenAI API) through your Teleport cluster's LLM gateways, powered by Teleport Beams.
(The name? A prism redirects beams.)
prism claude # Claude Code, routed via Teleport
prism codex # Codex CLI, routed via Teleport
prism opencode # OpenCode, routed via Teleport
prism pi # Pi, routed via Teleport
prism exec <cmd> # any other tool, with prism env vars set
No API keys to manage, no shared secrets, no .env files. Auth is
your Teleport identity.
- A Teleport cluster with Beams enabled. Beams provisions
cluster-wide apps (
anthropicandopenai) that proxy traffic to the upstream LLM APIs. Prism tunnels to these apps — it doesn't talk to Anthropic or OpenAI directly. tshon PATH and a validtsh loginto the Beams cluster. Prism usestsh proxy app(ortbotfor unattended use) to establish authenticated tunnels to the Beams apps.- Go ≥ 1.26 (only if building from source).
brew install webvictim/tap/prismgit clone https://github.com/webvictim/prism.git
cd prism
make
sudo make install # or: make install PREFIX=$HOME/.localRelease notes live in CHANGELOG.md and on the
releases page. Upgrading is
usually just prism down && prism up afterwards.
If you already have an interactive tsh login to a Beams-enabled
cluster, you can be running in 30 seconds:
tsh login --proxy=<your-cluster>.beams.run:443
prism config set proxy <your-cluster>.beams.run:443
prism claudePrism logs into both cluster apps, starts a local daemon, and execs
claude with the right env vars. Your tsh session expires after
12-24h; when it does, run tsh login again and prism's daemon
auto-recovers.
For unattended use (overnight agents, long jobs, CI), use tbot instead — it never expires. See the next section.
tbot is Teleport's Machine ID daemon. Prism runs it as a sidecar so
your AI tools have a self-refreshing identity that doesn't expire.
This is the right setup if you ever:
- leave Claude Code or Codex running overnight,
- use prism from CI / scripts / cron,
- don't want to think about
tsh loginever again.
One-time setup (assumes you have tctl admin perms on the cluster):
# 1. Generate Machine ID resource templates and a join token.
prism tbot bootstrap
# 2. Log in with admin perms so tctl can apply them.
tsh login --proxy=<your-cluster>.beams.run:443
# 3. Apply the three generated YAMLs (paths printed by bootstrap).
tctl create -f ~/.config/prism/tbot/role.yaml
tctl create -f ~/.config/prism/tbot/bot.yaml
tctl create -f ~/.config/prism/tbot/token.yaml
# 4. Fetch the registration secret and persist tbot config.
prism tbot configure
# 5. Switch prism over to the tbot backend.
prism config set identity tbot
prism config set tbot.dir ~/.config/prism/tbot
prism up
# 6. (Linux) Optionally install as a systemd user service for persistence.
prism installprism tbot bootstrap prints this exact sequence with your hostname
filled in (resources are named prism-bot-<hostname> so multiple
machines can share a cluster).
On Linux, prism install sets up a systemd user service so the daemon
starts on boot and survives logout — ideal for unattended use. Requires
lingering
enabled for your user (sudo loginctl enable-linger $USER).
Verify it's working:
prism tbot status # validates tbot dir, prints proxy/role/bot/secret state
prism status # shows daemon liveness + listener ports
prism test # smoke-tests all three wire formatsAfter that, every invocation of prism claude / prism codex /
prism opencode / prism pi / prism exec uses the tbot identity — no
re-login, ever.
Note: Teleport caps bot certificates at 12 hours (
DefaultBotMaxSessionTTL). Prism configures tbot to renew every 8 hours, well within this limit. If you see warnings about expired bot identities, runprism down && prism upto regenerate tbot.yaml with the correct renewal interval.
prism claude [args...] # run Claude Code through prism
prism codex [args...] # run Codex through prism
prism opencode [args...] # run OpenCode through prism
prism pi [args...] # run Pi through prism
prism exec <cmd> [args...] # run any command with prism env vars setAll of them auto-start the daemon if it isn't already running. Claude Code,
Codex, OpenCode and prism exec get endpoint environment variables pointing
at the local router. Pi ignores those variables, so prism pi also updates
Pi's model configuration before it starts. Any flags pass through:
prism claude --print "what's 2+2?"
prism codex --model openai.gpt-5.6-sol
prism opencode run "explain this repo"
prism pi -c
prism exec python my_script.pyFor tools you launch outside prism (an IDE plugin, a shell script), export the env vars yourself:
prism up
eval "$(prism env)"Two caveats specific to OpenCode, which is why prism opencode exists
rather than just prism exec opencode:
- It only offers a provider's models when one of the environment
variables named in its catalog is set —
ANTHROPIC_API_KEYfor Anthropic — soprism opencodesets it to a dummy value that the router strips before forwarding.eval "$(prism env)"does not set it, so OpenCode launched that way shows no Anthropic models unless you have already run/connect. - A
provider.anthropic.options.baseURLin your own OpenCode config takes precedence over the environment, and would silently send traffic straight to the vendor instead of through prism. If a session doesn't show up inprism logs, check for that first.
Claude Code disables Remote Control when
ANTHROPIC_BASE_URL is set (which prism does by default). To keep
Remote Control while still routing inference through Teleport, enable
forward-proxy mode:
prism config set claude_forward_proxy_mode true
prism down && prism upIn this mode, prism claude sets HTTPS_PROXY and
NODE_EXTRA_CA_CERTS instead of ANTHROPIC_BASE_URL. The daemon acts
as an HTTPS forward proxy: model API calls (/v1/messages) are
intercepted and routed through the Teleport tunnel, while everything
else (Remote Control, feature flags, telemetry, MCP) passes through to
the real api.anthropic.com with your credentials intact. A
locally-generated CA certificate (~/.config/prism/ca.pem) makes the
interception transparent to Claude Code.
Both interactive sessions and headless Remote Control work:
prism claude # interactive session, controllable from claude.ai/code
prism claude rc # headless Remote Control workerTo go back to plain base-URL mode:
prism config set claude_forward_proxy_mode false, then
prism down && prism up.
Prism tracks token consumption from every API request that flows through
the router. Usage is recorded per-request in monthly JSONL files under
~/.config/prism/usage/ and can be queried with prism usage:
prism usage # today's totals by model and proxy
prism usage --week # last 7 days
prism usage --all # all time
prism usage --json # raw JSONL records (pipe to jq)Example output:
prism usage (today, 42 requests)
────────────────────────────────────────────────────────────
Model Input Output
────────────────────────────────────────────────────────────
claude-opus-5 150.2k 28.4k cache: r=120.0k w=15.0k
claude-sonnet-5 45.0k 12.3k cache: r=30.0k w=5.0k
openai.gpt-5.6-sol 8.5k 3.2k
Proxy Input Output
────────────────────────────────────────────────────────────
prod.teleport.example.com:443 180.2k 35.6k
staging.teleport.example.com:443 23.5k 8.3k
────────────────────────────────────────────────────────────
TOTAL 203.7k 43.9k
The per-proxy breakdown is useful for tracking consumption against cluster-level token caps.
Pi ignores ANTHROPIC_BASE_URL and
OPENAI_BASE_URL; it reads base URLs from its model registry instead. Use the
first-class launcher and prism handles both pieces:
prism pi [args...]It starts the daemon, gives Pi dummy Anthropic and OpenAI keys, mirrors Pi's
catalog into ~/.pi/agent/models.json with each baseUrl pointed at the local
router, then starts Pi. On a fresh Pi install it first runs
pi update --models to populate the catalog. If that refresh cannot reach
Pi's catalog service, the launcher stops rather than starting Pi with direct
vendor routes. PI_CODING_AGENT_DIR is respected when Pi's configuration
lives somewhere else.
Pi's overrides match by model id, so prism copies the ids and each model's
cost, context and compatibility metadata from models-store.json; no model
name is compiled into prism. Existing custom providers in models.json, such
as llama-swap, are left alone. Each prism pi launch refreshes the Anthropic
and OpenAI entries from the current catalog and router port.
The older setup-only command remains available when you want to route only one model from either provider:
prism pi config --openai-model gpt-5.6-sol
prism exec piUse prism exec pi after a narrowed setup. A normal prism pi restores every
catalog model before launching. Pi also has its own unrelated config command;
run that as prism exec pi config because prism pi config belongs to prism.
Newer gateways serve OpenAI models only on the Responses API. Ask for
any model on /v1/chat/completions and you get:
model `openai.gpt-5.6-sol` isn't supported on this route
Clients that only speak chat/completions — MacWhisper, Teleport session
summaries, most "OpenAI-compatible endpoint" boxes — would be dead in the
water. So prism translates: /v1/chat/completions in, /v1/responses
upstream, chat-shaped reply back out, streaming included. Requests to
/v1/responses are untouched, so Codex and anything else already speaking
Responses is unaffected.
It's on by default. Turn it off to talk to an older gateway that still serves chat/completions natively:
prism config set openai_chat_completions_shim false
prism down && prism upWith it off, /v1/chat/completions is relayed byte-for-byte, which is what
makes a current prism usable against a legacy Beam.
Translated requests are visible in the log:
POST /v1/chat/completions [-> /v1/responses] 200 req=141B resp=373B model=openai.gpt-5.6-luna in=11 out=9 cache_read=0 cache_write=0 2.95s
No model names are compiled into prism. Reasoning models reject
parameters like temperature and top_p, but which ones depends on the
model, so prism doesn't guess: it forwards what the client sent, reads the
parameter name out of the gateway's rejection, drops it, and retries —
remembering the rejection per model for the life of the daemon.
chatcompat: openai.gpt-5.6-sol rejected parameter "temperature" — dropped it,
retrying, and remembering for the rest of this daemon's life
The memory is deliberately in-process, not on disk: a persisted cache would keep stripping a parameter forever after the gateway started accepting it. Relearning costs one extra round trip per model per restart.
Two limitations worth knowing:
- Tool calling isn't translated. A request carrying
toolsgets a 400 pointing at/v1/responses. - Reasoning tokens count against
max_tokens. A client that asks formax_tokens: 40may spend all of it on reasoning and getfinish_reason: "length"with little or no text. That's the gateway's accounting, not prism's — raise the client's limit if replies come back truncated.
| Command | What it does |
|---|---|
prism claude [args...] |
Ensures the daemon is up; execs claude with prism env. |
prism codex [args...] |
Same, for codex. |
prism opencode [args...] |
Same, for opencode, plus a dummy ANTHROPIC_API_KEY so it surfaces the Anthropic models. |
prism pi [args...] |
Same, for Pi, plus automatic model-catalog configuration. |
prism exec <cmd> [args...] |
Same, for any command. |
prism up |
Starts the local daemon (tunnels + router). Idempotent. |
prism down |
Stops the daemon and logs out of the apps. |
prism status |
Port assignments, identity state, daemon liveness. |
prism env |
Prints export statements for your shell to eval. |
prism logs |
Tails the local daemon log (request-level logging). |
prism test [anthropic|openai|all] |
Smoke test. --format anthropic|openai-responses|openai-completions picks a wire format, --model a model (default: let the gateway choose), --stream exercises SSE. |
prism usage [--week|--all|--json] |
Show token usage by model and proxy. |
prism pi config |
Configure Pi without launching it. --anthropic-model / --openai-model narrow either provider to one id. |
prism config [show|set|unset|clear] |
View/edit persistent config (proxy, identity, tbot.dir, claude_forward_proxy_mode, openai_chat_completions_shim). |
prism tbot bootstrap |
Generate Machine ID resources for tbot identity. |
prism tbot configure |
Persist the bound-keypair registration secret. |
prism tbot status |
Validate the tbot working directory. |
prism install |
Install as a system service (Linux systemd / macOS LaunchAgent). |
prism uninstall |
Remove the system service. |
prism version |
Print build version. |
The daemon was killed. prism down && prism up.
If the daemon was SIGKILL'd on macOS/Linux, the tbot subprocess gets
reparented to PID 1 and keeps holding its port. pkill -x tbot and
retry. (Windows uses Job Objects, so this can't happen there.)
The bot's role doesn't grant access to the cluster's LLM apps. Re-run
prism tbot bootstrap and re-apply the role:
tctl create -f ~/.config/prism/tbot/role.yamltbot refuses to write to its storage directory if the path contains
symlinks (common in Lima VMs where ~/.config is symlinked). Move the
tbot directory to a real path:
prism tbot bootstrap --dir ~/prism-tbot
prism config set tbot.dir ~/prism-tbot
# then re-run tctl create and prism tbot configure as normalThe cluster's Anthropic gateway is Bedrock-backed and rejects some
request fields the first-party API accepts (top-level fields such as
thinking and Pi's fallbacks, plus unknown keys inside cache_control).
Prism strips the known ones; if a new one shows up, turn on debug logging and
check the daemon log:
prism down
PRISM_DEBUG=1 prism up
prism logs # in another terminal, reproduce the errorThen add the offending field to anthropicStripFields (or
cacheControlAllowedKeys) in internal/scrub/anthropic.go. To find
the culprit, replay the failing request body against
http://127.0.0.1:7331/v1/messages with fields removed until it
succeeds.
Run tsh login — the daemon detects the refreshed identity and
restarts its subprocesses automatically. If you're tired of this,
switch to tbot (see above).
Enable forward-proxy mode — see
Claude Code Remote Control above. Both
interactive sessions and headless prism claude rc work in that mode.
If prism is installed as a service and misbehaving, these commands help:
macOS (LaunchAgent):
# Check if the job is loaded (modern launchctl)
launchctl print gui/$(id -u)/com.prism.daemon
# Stop and restart
launchctl bootout gui/$(id -u)/com.prism.daemon
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.prism.daemon.plist
# View the plist
cat ~/Library/LaunchAgents/com.prism.daemon.plist
# Tail daemon logs
prism logsLinux (systemd):
# Service status + recent logs
systemctl --user status prism
# Full logs (follow mode)
journalctl --user -u prism -f
# Restart
systemctl --user restart prism
# Check if lingering is enabled (survives logout)
ls /var/lib/systemd/linger/$USER┌────────────────────────┐
│ Claude Code / Codex / │ ANTHROPIC_BASE_URL=http://127.0.0.1:7331
│ any AI tool │ OPENAI_BASE_URL=http://127.0.0.1:7331/v1
└──────────┬─────────────┘
│ plain HTTP
▼
┌────────────────────────┐
│ prism router (:7331) │ path-based dispatch + Bedrock scrubbing
│ /v1/messages → :7333 │
│ /v1/chat/* → :7334 │
└────┬──────────────┬────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ tsh/tbot│ │ tsh/tbot│ tunnel subprocesses
│ proxy │ │ proxy │
│anthropic│ │ openai │
└────┬────┘ └────┬────┘
│ │
▼ ▼
Teleport Teleport Beams-managed LLM gateway apps
anthropic openai
app app
│ │
▼ ▼
Anthropic OpenAI
(Bedrock) API
The router (internal/router/) dispatches by path; the shared scrub
package (internal/scrub/) makes requests Bedrock-compatible (strips
fields like thinking and metadata, sanitizes cache_control, caps
non-streaming max_tokens at 8192 — Bedrock requires streaming above
that). For OpenAI requests, it renames max_tokens to
max_completion_tokens, which newer models require. /v1/chat/completions
is handled by internal/chatcompat/ (see
chat/completions shim). The router also
captures token usage from responses into ~/.config/prism/usage/. The
tunnels are tsh proxy app subprocesses, or — in tbot mode — a single
tbot start with two application-tunnel services.
In forward-proxy mode (see
Claude Code Remote Control), the same
router also accepts HTTPS CONNECT and plain forward-proxy requests:
api.anthropic.com model calls are intercepted and sent through the
anthropic tunnel with the same scrubbing, and everything else passes
through untouched.
cmd/prism/ local CLI + daemon
internal/router/ HTTP router with path dispatch + usage capture
internal/scrub/ Bedrock/OpenAI request scrubbing (shared)
internal/mitm/ forward proxy for Remote Control compatibility
internal/tunnel/ subprocess supervisor
internal/tbot/ tbot config rendering and bootstrap
internal/identity/ tsh session expiry watcher
internal/state/ runtime state persistence
internal/config/ persistent machine config
internal/usage/ token usage tracking (JSONL writer + reader)
internal/logfile/ rotating daemon log writer
internal/tshwrap/ tsh CLI wrappers
Makefile build targets for all platforms
Apache 2.0 — see LICENSE.