Important
<extremaly professor farnsworth voice> GOOD NEWS, EVERYONE!
- Things will be less insanely cheap, BUT
- claude-ds should be a lot smarter.
There was one missed case from reverse-engineering claude where a fair number of tool-related
requests were coming to the proxy as Opus-4.7 and being routed to deepseek 4 flash :(
This has been fixed as of 0.8.0! Hooray OTLP! Logs and charts to verify!
Run Claude Code against DeepSeek's Anthropic-compatible API — with system-keychain / 1Password / Infisical secret refs, schema-versioned config that auto-migrates and auto-repairs, lazy-installed sidecar proxy, end-to-end
--doctordiagnostics, and per-tier reasoning controls when you want them.
claude-ds is a small Bash wrapper plus an optional Python sidecar. It
exists because pointing Claude Code at a third-party Anthropic-compatible
endpoint is a tangle of environment variables, model-id gates, schema
drift, and an incompatible reasoning-depth wire format. claude-ds
makes that one command:
claude-dsThe wrapper takes care of:
- prompting for and storing your secret reference on first run
- testing your API key against DeepSeek before saving
- migrating older config files forward when the schema changes
- detecting and auto-repairing damaged configs (with a backup so nothing is lost)
- lazy-installing the optional reasoning-effort proxy if and when you opt in
- gracefully falling back when an optional dependency (python3, curl) is missing
If something does go wrong, claude-ds --doctor runs an end-to-end
checklist and tells you exactly what to fix.
- Why use this
- Quickstart
- Installation
- What the wrapper does for you — the important section if you'd rather not read the rest
- Configuration — start here if you don't know what to set
- Secret references
- Reasoning-effort proxy
- Auto-mode unlock
- Per-tier model overrides
- Visual branding (tmux)
- Environment variables
- Troubleshooting
- Developer notes
- License
| Problem | What claude-ds does |
|---|---|
Setting ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN by hand on every shell session |
Persists a single config file under $XDG_CONFIG_HOME/claude-ds/. |
Hard-coding API keys in shell rc files or .env |
Stores a reference (op://, system://, infisical://, …) and resolves it on each run. Plaintext is supported but never required. |
Claude Code's auto-mode (--auto permission classifier) refuses to run on non-Anthropic models |
Optional unlock_auto_mode flag spoofs the wire-level model id so the gate passes. |
/model default and tier routing break against single-tier providers |
All four tiers (opus / sonnet / haiku / small_fast) are pinned to the configured model by default; per-tier overrides are available. |
DeepSeek doesn't honor Anthropic's thinking.budget_tokens; Claude Code can't send reasoning_effort |
A local Python proxy translates between the two — think hard → medium, ultrathink → high, etc. — fully configurable, off by proxy_effort=off. |
Claude Code uploads images via the Anthropic Files API (POST /v1/files); DeepSeek doesn't implement it |
The proxy intercepts file uploads, caches the image as base64, and rewrites any file_id references in outgoing /v1/messages to inline base64 that DeepSeek accepts. Fully transparent — attach images in Claude Code exactly as you normally would. |
| Hard to tell at a glance which terminal is talking to DeepSeek vs. real Anthropic | When run inside tmux, the active pane gets a DeepSeek-themed top border and the window name is prefixed 🐋. |
| Config schema changes between releases | Schema-versioned (_schema=N); old configs are auto-migrated forward with a .v<old>.bak backup. |
| Hand-edited config got broken | claude-ds detects malformed lines on launch, backs up the original to config.broken.<timestamp>.bak, and rewrites a clean config preserving every parseable key. |
| "Did I install everything correctly?" | claude-ds --doctor checks claude on PATH, python3, proxy script presence, secret-ref resolves, API key live against upstream, and tier-collision lint — printing ✓/✗ with actionable next steps. |
# Zero to configured in one command:
curl -fsSL https://raw.githubusercontent.com/earchibald/claude-ds/main/install.sh | bashThe installer automatically resolves the latest GitHub Release — you get a known-good, tagged version without us having to update a version number in this README. Each release is published on the GitHub Releases page with a changelog extracted from CHANGELOG.md.
To pin a specific version: CDS_INSTALL_REF=v0.7.4 curl -fsSL .../main/install.sh | bash
The installer asks where to install (~/.local/bin by default, or
/usr/local/bin, or a custom path), handles sudo transparently when
the target directory isn't user-writable, gracefully handles existing
installs (offers to overwrite) and existing configs (keep / backup /
overwrite), then runs first-time onboarding via claude-ds --setup.
Onboarding walks you through:
- secret reference — paste a key directly, or use
system://,op://,infisical://(see Secret references) - liveness check — verifies your key against DeepSeek before saving
- reasoning-effort proxy opt-in — default off; most users don't need it
After onboarding, the installer exits cleanly — no claude session
is launched. Run claude-ds when you're ready.
# Useful follow-ups:
claude-ds --doctor # end-to-end checklist if anything seems wrong
claude-ds --rotate-key # rotate the stored API key💡 The proxy script doesn't need
chmod +x— the wrapper invokes it aspython3 claude-ds-proxy.py. Onlyclaude-dsitself must be executable.💡 Symlinking is supported.
claude-dsresolves its own symlinks before locating the proxy script, soln -s ~/src/claude-ds/claude-ds ~/.local/bin/claude-dsworks — the proxy is found in the source tree without a second symlink. The two files must be siblings on the real filesystem, not in~/.local/bin.
⚠️ Your API key lives in the process environment at runtime. Whichever secret reference scheme you use, the resolved key is exported asANTHROPIC_AUTH_TOKENtoclaudeand to every processclaudespawns (MCP servers, shell tools, subprocesses). If your threat model includes untrusted MCP servers or tool output, treat the key as ephemeral and rotate regularly. The on-disk config ischmod 600, but at-rest protection does not extend to the running process tree.
curl -fsSL https://raw.githubusercontent.com/earchibald/claude-ds/main/install.sh | bashThe installer auto-resolves the latest GitHub Release and downloads from that tag. See the Releases page for the current latest and its changelog.
The installer is interactive — it asks where to install, handles sudo,
and runs first-time onboarding. See Quickstart for what
to expect.
Set CDS_INSTALL_REF to any tag, branch, or SHA:
CDS_INSTALL_REF=v0.7.4 curl -fsSL https://raw.githubusercontent.com/earchibald/claude-ds/main/install.sh | bashRe-run the installer — it fetches the latest release automatically:
curl -fsSL https://raw.githubusercontent.com/earchibald/claude-ds/main/install.sh | bashThe installer detects an existing install and offers to overwrite.
Your config (~/.config/claude-ds/config) is never touched — the
installer only replaces the wrapper and proxy script binaries.
mkdir -p ~/.local/bin
# Replace v0.8.0 with the latest tag from the Releases page.
curl -fL https://raw.githubusercontent.com/earchibald/claude-ds/v0.8.0/claude-ds -o ~/.local/bin/claude-ds
curl -fL https://raw.githubusercontent.com/earchibald/claude-ds/v0.8.0/claude-ds-proxy.py -o ~/.local/bin/claude-ds-proxy.py
chmod +x ~/.local/bin/claude-ds
# Both files must share a directory — the wrapper resolves
# `claude-ds-proxy.py` from the directory of its own real path.Make sure ~/.local/bin is on $PATH.
If you need an unreleased fix or want to test a PR before it lands in a
release, force the installer to pull from main:
CDS_INSTALL_REF=main curl -fsSL https://raw.githubusercontent.com/earchibald/claude-ds/main/install.sh | bash| Required for | |
|---|---|
| Bash 4+ | the wrapper itself (macOS ships 3.2 — install bash via Homebrew, or run with the /bin/bash your distro provides) |
claude CLI on $PATH |
obvious |
| Python 3.8+ | only when the reasoning-effort proxy is enabled (the default — set proxy_effort=off to skip) |
op CLI |
only for op:// secret references |
infisical CLI |
only for infisical:// secret references |
secret-tool (Linux, libsecret) |
only for system:// references on Linux. macOS uses the built-in security command. |
git clone https://github.com/earchibald/claude-ds.git
cd claude-ds
./claude-ds --version
# Or symlink it:
ln -s "$PWD/claude-ds" ~/.local/bin/claude-dsThe wrapper resolves the proxy script relative to its own real path (after symlink resolution), so running directly from the source tree works without a separate proxy symlink.
claude-ds --version # `claude-ds X.Y.Z`, a horizontal rule, then `claude --version`.
# For machine parsing: `claude-ds --version | head -1`.
claude-ds --help # full help, paged through $PAGER (falls back to less / more / cat)
claude-ds --doctor # end-to-end checklist (claude on PATH, python3, proxy script,
# secret-ref resolves, API key live, tier collisions)You should rarely need to read this README beyond the Quickstart.
claude-ds is designed to do its own onboarding, maintenance, and
self-healing:
| Situation | What claude-ds does automatically |
|---|---|
| Fresh install, no config | Interactively prompts for a secret reference (with helpers for system:// and infisical://), liveness-checks the resulting API key against DeepSeek, and offers an opt-in for the reasoning-effort proxy. |
| Old config from a previous version | Detects the schema version, backs up the original to config.v<old>.bak, and migrates forward in place. |
| Damaged config (typo, hand-edit gone wrong) | Detects malformed lines on launch, backs up the original to config.broken.<timestamp>.bak, drops the bad lines with a named warning, preserves every parseable key, and continues. Nothing is lost. |
| Missing proxy script | If the proxy is enabled, attempts a one-time curl from raw.githubusercontent.com next to the wrapper. On success, runs normally. On failure, soft-falls-back to proxy-disabled for the session and warns — the config is never mutated. |
| Missing python3 | Soft-fall-back to proxy-disabled for the session, with a single warning telling you what to install. The wrapper still launches claude normally. |
| Missing curl | Same soft-fallback, with a different warning. |
| Bad API key | First-run prompt re-asks (up to 3 times). Subsequent launches surface the failure via --doctor. |
| Symlinked install | The wrapper resolves its own symlinks before locating the proxy script — so ln -s ~/src/claude-ds/claude-ds ~/.local/bin/claude-ds Just Works. |
| Per-tier proxy specs that would silently collide | Linted on every launch; warns the moment two tiers map to the same wire id and tells you which tier wins. |
| API key rotation | claude-ds --rotate-key (or --reset-password) — interactive, liveness-checked, preserves your proxy_effort choice across rotation. |
| Anything else seems wrong | claude-ds --doctor walks the full checklist with ✓/✗ and an actionable next step on each failure. |
The rest of this README is reference material. If you only ever read the sections above, you should be fine.
claude-ds reads a single config file:
${XDG_CONFIG_HOME:-$HOME/.config}/claude-ds/config
The file is created on first run with safe defaults and chmod 600. It uses
key=value pairs, one per line. Lines beginning with # and blank lines are
ignored — comment a key out to fall back to its built-in default.
Most users want one of these. Copy, adjust the secret reference, save to
~/.config/claude-ds/config, run claude-ds. Done.
A. "Just give me Claude Code on DeepSeek." (the default)
_schema=1
api_key_ref=op://Private/DeepSeek/credential
base_url=https://api.deepseek.com/anthropic
model=deepseek-v4-pro
proxy_effort=offDeepSeek's compat shim already maps claude's /think, /think-hard,
and /ultrathink commands to its native reasoning regime (high, or
max for ultrathink-tier budgets). The proxy is off by default — no
sidecar process is spawned, and claude talks to DeepSeek directly.
B. "I want auto-mode (--auto permission classifier) to work."
_schema=1
api_key_ref=op://Private/DeepSeek/credential
base_url=https://api.deepseek.com/anthropic
model=deepseek-v4-pro
unlock_auto_mode=1
capabilities=effort,thinking
proxy_effort=offSpoofs Claude-canonical model ids so the auto-mode gate passes, and
advertises effort / thinking capabilities so claude doesn't strip
them on the way out.
C. "I want to force a specific reasoning regime regardless of what claude asks for."
This is when you opt into the proxy. For example, "always reason at max on opus, default behaviour everywhere else":
_schema=1
api_key_ref=op://Private/DeepSeek/credential
unlock_auto_mode=1
proxy_effort=off # don't change behaviour for sonnet/haiku/small_fast
proxy_effort_opus=max # always reason at max on the opus tierOr "always at least default reasoning, even on routine requests":
proxy_effort=auto:highOr "save tokens by never reasoning":
proxy_effort=none
⚠️ Per-tier specs key on the wire-level model id, not the tier name. Read the tier-collision rules before mixing per-tier specs — collisions matter most whenunlock_auto_modeis off (all tiers share one wire id) or whenunlock_auto_mode=1makes haiku and small_fast both map toclaude-haiku-4-5.claude-dslints for collisions on launch and prints a warning naming the colliding tiers.
Keys in bold change behaviour on every run. The rest are debugging or advanced overrides.
| Key | Default | Purpose |
|---|---|---|
_schema |
1 |
Config schema version. The wrapper migrates older versions forward on launch (with a .v<old>.bak backup). |
api_key_ref |
(set on first run) | Secret reference for the DeepSeek API key. See Secret references. |
base_url |
https://api.deepseek.com/anthropic |
Upstream Anthropic-compat endpoint. Override to point at a different gateway. |
model |
deepseek-v4-pro |
Default DeepSeek model id sent over the wire. |
model_opus |
(unset → uses model) |
Override wire-level model for the opus tier. |
model_sonnet |
(unset → uses model) |
Override wire-level model for the sonnet tier. |
model_haiku |
(unset → uses model) |
Override wire-level model for the haiku tier. |
model_small_fast |
(unset → uses model) |
Override wire-level model for the small-fast tier. |
capabilities |
(unset) | Comma-separated capability list advertised to claude per tier. e.g. effort,thinking,adaptive_thinking,interleaved_thinking. Set only when the upstream gateway actually supports them. |
unlock_auto_mode |
(unset) | When 1, spoofs Claude-canonical model ids to satisfy auto-mode's regex gate. See Auto-mode unlock. |
proxy_effort |
off |
Global default for the reasoning-effort proxy. Spec language documented there. The proxy is opt-in — off means the wrapper never spawns the Python child. |
proxy_effort_opus |
(unset) | Per-tier override for opus. Subject to wire-id collision — see Per-tier collisions. |
proxy_effort_sonnet |
(unset) | Per-tier override for sonnet. Subject to collision. |
proxy_effort_haiku |
(unset) | Per-tier override for haiku. Subject to collision. |
proxy_effort_small_fast |
(unset) | Per-tier override for the small-fast tier. Subject to collision. |
proxy_bind |
127.0.0.1 |
Interface the proxy listens on. Leave at loopback unless you have a specific reason. |
proxy_debug |
0 |
When 1, logs every regime application to stderr. |
📝 The deprecated
proxy_strip_thinkingkey (v0.5) is silently ignored — strip/preserve behaviour is now baked into the regime model (nonestrips,high/maxpreserve).
api_key_ref accepts one of four schemes:
| Scheme | Resolved by | Example |
|---|---|---|
<bare-key> |
nothing — written to disk as plaintext | sk-deepseek-abc123… |
op://VAULT/ITEM/FIELD |
1Password CLI (op read) |
op://Private/DeepSeek/credential |
system://<account> |
OS keychain — security on macOS, secret-tool on Linux. Service name is claude-ds. |
system://default |
infisical://PROJECT/ENV/PATH#KEY |
Infisical CLI (infisical secrets get) |
infisical://abc123/prod/#DEEPSEEK_API_KEY |
✱ Shorthand: for
op,system, andinfisicalyou may type the scheme name without://(e.g.system,infisical) andclaude-dswill append it for you.
When the config file doesn't exist, claude-ds prompts for a reference.
Two interactive helpers kick in automatically:
system://— drops into a numbered selector listing existing keychain entries under serviceclaude-ds. Pick by number, type a new account name, or hit enter to be prompted for one. If the chosen account already has a stored secret, it is reused silently; otherwise you are prompted (with asterisk-echoed input) for the key, which is then stored in the keychain.infisical://— walks an interactive builder asking for project ID, environment slug (defaultdev), folder path (default/), and secret key, then constructs the full URI for you.
claude-ds --reset-passwordFor system://<account> references, the reset flow asks whether to
[1] replace the stored secret while keeping the account name, or
[2] switch to a different account or scheme entirely (with a follow-up
asking whether to delete the old keychain entry).
For non-system references, the local config reference is forgotten and you are re-prompted; upstream stores (1Password, Infisical) are never touched.
Off by default. You don't need this for normal use. DeepSeek already maps claude's
/think,/think-hard, and/ultrathinkinto its native reasoning regime via thethinkingblock claude already sends. Enable the proxy only when you want to force a specific regime regardless of what claude requests — e.g. always-max for tough work, always-none for token savings, or per-tier knobs.
DeepSeek's compat shim recognises three reasoning regimes:
| Regime | Wire shape | Behaviour |
|---|---|---|
none |
thinking block absent, no reasoning_effort |
No reasoning. |
high |
thinking: {type: enabled} present, reasoning_effort absent (or =high — same wire effect) |
DeepSeek's default reasoning depth. |
max |
thinking: {type: enabled} present and reasoning_effort=max |
Maximum reasoning. |
Anthropic's API uses thinking.budget_tokens (an integer) for the same
idea. The proxy translates between them and collapses any
caller-supplied Anthropic-style levels (low, medium, xhigh,
etc.) onto these three regimes — DeepSeek would otherwise reject them.
For every POST /v1/messages it sees:
- Determine the source bucket from the incoming body's
thinkingblock:- no thinking block →
none - thinking enabled, budget
< 31000→high - thinking enabled, budget
>= 31000→max(ultrathink)
- no thinking block →
- Resolve the per-model spec → target regime.
- Apply the regime's transformation in place:
none→ strip boththinkingandreasoning_efforthigh→ ensurethinking: {type: enabled}(preserving any caller-suppliedbudget_tokens), stripreasoning_effortmax→ ensurethinking: {type: enabled}, setreasoning_effort=max
The value of proxy_effort (and each per-tier proxy_effort_*):
| Value | Behaviour |
|---|---|
off (or empty) |
Pass the body through unchanged. The proxy is a no-op for this model. |
auto |
Mirror the source bucket: none → strip, high → ensure thinking on, max → ensure thinking + max. |
auto:<level> |
Like auto, but upgrade the no-thinking case to <level>. auto:high forces thinking on every request; auto:max forces full reasoning whenever claude is silent. |
none |
Always strip thinking and reasoning_effort (force no-reasoning). |
high |
Always ensure thinking enabled + drop reasoning_effort (force default reasoning). |
max |
Always ensure thinking + reasoning_effort=max (force maximum reasoning). |
none=<v>|high=<v>|max=<v> |
Full per-source-bucket matrix. Clauses are optional; missing buckets pass through unchanged. |
- Look up the model id in the per-tier map (built from
proxy_effort_*and the wire-level model id of each tier). On a hit, use that spec. - Otherwise, fall back to the global
proxy_effortspec. - If the resolved spec is
off(or unset), the body is forwarded unchanged. - Otherwise, apply the regime's transformation.
You can opt in three ways:
# (a) Interactive — first-run prompt offers proxy choices
claude-ds # prompts on first run
# (b) One-shot via env var
CLAUDE_DS_PROXY_EFFORT=auto:high claude-ds
# (c) Persistently in config
echo "proxy_effort=auto:max" >> ~/.config/claude-ds/configWhen the proxy is enabled, claude-ds will lazy-install the
sidecar script if it's missing — fetching claude-ds-proxy.py from
the same source the wrapper came from. If the download fails (or
curl is missing, or python3 is missing), the wrapper soft-falls-back
to running with the proxy disabled for that session and prints a
single warning. Your config is never silently mutated.
proxy_effort=off
# (and leave every per-tier proxy_effort_* unset or off)When all specs resolve to "off", claude-ds skips spawning the Python child
entirely; ANTHROPIC_BASE_URL is exported pointing straight at DeepSeek and
there is zero proxy overhead.
CLAUDE_DS_PROXY_EFFORT=off claude-ds # skip the proxy this invocation
CLAUDE_DS_PROXY_EFFORT=auto:max claude-ds # opt in for one run
CLAUDE_DS_PROXY_DEBUG=1 claude-ds # log every regime application to stderrThe proxy keys its lookup table on the wire-level model id (the value
of ANTHROPIC_DEFAULT_<TIER>_MODEL), not the tier name. Whenever two
tiers share a wire id, only one per-tier spec can win for that id.
claude-ds writes per-tier specs into the lookup table in the order
small_fast → haiku → sonnet → opus — so for any given wire id, the
tier later in that order wins on collision.
In practice that means three regimes:
| Configuration | Wire ids per tier | Collisions | Effective rule |
|---|---|---|---|
Default (no unlock_auto_mode, no model_* overrides) |
All four → deepseek-v4-pro |
All four collide | proxy_effort_opus wins for every tier; the others are dead config. |
unlock_auto_mode=1, no model_* overrides |
opus → claude-opus-4-7, sonnet → claude-sonnet-4-6, haiku & small_fast → claude-haiku-4-5 |
haiku ↔ small_fast | opus and sonnet behave independently. proxy_effort_haiku wins over proxy_effort_small_fast for the haiku/small_fast wire id. |
Distinct model_<tier> overrides (or unlock_auto_mode=1 with at least one tier overridden to a unique id) |
Four distinct ids | None | All four per-tier specs are independent. |
If you set per-tier specs that you expect to be independent, double-check
which regime you're in. The simplest fix for an unwanted collision is to
add a model_<tier> override that gives the tier its own wire id, or to
enable unlock_auto_mode=1 if it isn't already.
Claude Code's auto mode — the permission classifier that auto-approves routine tool calls — is gated on the model id matching one of:
claude-opus-4-7
claude-opus-4-6
claude-sonnet-4-6
(The provider is not checked; just the model name regex.) With the default
model=deepseek-v4-pro, auto mode reports "unavailable for this model".
Setting unlock_auto_mode=1 makes claude-ds advertise spoofed Anthropic
model ids to claude:
ANTHROPIC_MODEL=claude-opus-4-7
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-7
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5
ANTHROPIC_SMALL_FAST_MODEL=claude-haiku-4-5
The picker labels (ANTHROPIC_DEFAULT_*_MODEL_NAME /
ANTHROPIC_DEFAULT_*_MODEL_DESCRIPTION) are also set to DeepSeek-branded
strings so /model still shows what's actually running over the wire.
Whether this works depends on your gateway: DeepSeek's compat shim accepts
arbitrary model values (it routes by URL+auth) at the time of writing, but
some gateways reject unknown model ids. If yours does, leave
unlock_auto_mode unset.
By default, all four tiers (opus, sonnet, haiku, small_fast) are
pinned to the value of model. This keeps /model default and tier-routing
working against a single-tier provider.
To run different DeepSeek models per tier, set any subset of:
model=deepseek-v4-pro
model_opus=deepseek-v4-pro
model_sonnet=deepseek-v4-mid
model_haiku=deepseek-v4-fast
model_small_fast=deepseek-v4-fastPer-tier model_* overrides also win over the auto-mode-unlock spoofed
ids, so you can have genuine claude-opus-4-7 spoofing on opus while running
real DeepSeek ids on the cheaper tiers.
When claude-ds is launched inside tmux, the active pane gets a DeepSeek
indigo top border with a 🐋 badge:
─🐋 DEEPSEEK ─ model: deepseek-v4-pro · wire id: claude-opus-4-7 (spoofed for auto-mode) ────
The window name in tmux's status bar is also prefixed 🐋 so the marker
is visible from anywhere.
To skip branding (e.g. for headless claude -p runs), set:
CLAUDE_DS_NO_BRANDING=1 claude-ds -p "summarise this file"If the indigo (#4D6BFE) looks washed-out in iTerm2, lower the
Minimum Contrast slider in Profiles → Colors. Also confirm your
tmux config has terminal-features with RGB set for your $TERM so
24-bit color passes through.
Variables claude-ds reads (one-shot overrides):
| Variable | Effect |
|---|---|
CLAUDE_DS_PROXY_EFFORT |
Overrides proxy_effort for this invocation. off skips the proxy. |
CLAUDE_DS_PROXY_DEBUG |
When 1, the proxy logs each injection decision to stderr. |
CLAUDE_DS_NO_BRANDING |
When set, suppresses tmux branding. |
INFISICAL_TOKEN |
Used by the Infisical CLI when resolving infisical:// refs without an interactive login. |
XDG_CONFIG_HOME |
Where the config file lives ($XDG_CONFIG_HOME/claude-ds/config). |
PAGER |
Used by --help (falls back to less -RF, then more, then cat). |
TMUX, TMUX_PANE |
Auto-detected for the visual-branding block. |
Variables claude-ds exports to claude:
| Variable | Set when |
|---|---|
ANTHROPIC_BASE_URL |
always (points at the proxy when enabled, DeepSeek directly otherwise) |
ANTHROPIC_AUTH_TOKEN |
always (the resolved API key) |
ANTHROPIC_MODEL |
always |
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL |
always |
ANTHROPIC_SMALL_FAST_MODEL |
always |
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL_NAME / _DESCRIPTION |
when unlock_auto_mode=1 (DeepSeek-labelled picker text) |
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL_SUPPORTED_CAPABILITIES |
when capabilities= is set |
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 |
always |
CLAUDE_DISABLE_NONSTREAMING_FALLBACK=1 |
always |
CLAUDE_DS=1 |
always (marker — useful for hooks / statusline scripts; e.g. if [[ -n "$CLAUDE_DS" ]]; then echo "DeepSeek session"; fi) |
claude-ds: python3 not found
The reasoning-effort proxy needs Python 3.8+. Install it, or set
proxy_effort=off (and clear all per-tier proxy_effort_*) to bypass.
claude-ds: reasoning-effort proxy failed to start
Re-run with the env override CLAUDE_DS_PROXY_DEBUG=1 claude-ds — the
proxy's startup error will print to stderr. (proxy_debug=1 is the
config-file equivalent and requires editing the file before re-running.)
Common causes: a malformed spec in proxy_effort_* (the parser names the
offending clause), or proxy_bind pointing at an interface you don't have.
Auto mode says "unavailable for this model"
Set unlock_auto_mode=1. If your gateway rejects spoofed claude-* ids,
auto mode is genuinely unavailable on that provider.
/model default shows the same DeepSeek model for every tier
That's expected on a single-tier provider. Set distinct model_<tier>
overrides if your DeepSeek plan has tiered models.
My API key didn't get persisted
You probably entered it as a bare plaintext key. Plaintext is stored
verbatim in the config (chmod 600); to use the OS keychain instead, run
claude-ds --reset-password and enter system://.
The tmux border is invisible / washed out
Lower iTerm2's Minimum Contrast slider (Profiles → Colors). For
non-iTerm terminals, ensure your tmux.conf has terminal-features with
RGB for your $TERM.
The proxy keeps running after I quit claude
The proxy has an orphan watchdog (in claude-ds-proxy.py) that polls
os.getppid() every 2 seconds and exits when reparented to PID 1
(init/launchd). On macOS and on Linux without user-level subreapers
this is reliable.
⚠️ Linux +systemd --usercaveat. Whensystemd --useris the session leader, orphans may be reparented to the user-systemd PID instead of PID 1, and the watchdog won't trigger. Check with:ps -o ppid= -p "$(pgrep -f claude-ds-proxy.py)"If that prints anything other than
1, kill manually withpkill -f claude-ds-proxy.py. (We're tracking a fix usingprctl(PR_SET_PDEATHSIG)on Linux — PRs welcome.)
Status:
claude-dsis now a standalone repository, graduated from theearchibald/agent-utilitiesmonorepo. The canonical source lives here. The original commit history remains in the monorepo; this repo starts fresh. PRs and issues should target this repository.
├── claude-ds # Bash wrapper (entry point)
├── claude-ds-proxy.py # Python 3 stdlib HTTP proxy (request rewriter)
├── install.sh # curl | bash installer
├── README.md # this file
├── tests/
│ └── test_proxy_images.py # TDD test suite for image/Files API proxy
├── CDS-4-MANIFEST.md # [[CDS-4-MANIFEST]] — change log for CDS-4 image proxy work
└── docs/
├── claude-ds.md # user-facing guide
├── deepseek-vision-research.md # [[deepseek-vision-research]] — research findings, attempt log, and constraints for DeepSeek vision integration
├── secretref-lib.md # embedded reusable secretref library docs
└── infisical-adapter.md
┌───────────────────────┐
user ─► claude-ds (bash) │ secretref library │
│ │ (op:// system:// │
│ resolve ref ───────────► infisical://) │
│ └───────────────────────┘
│ build effort map from per-tier configs
│ spawn proxy if needed
│
▼
ANTHROPIC_BASE_URL=http://127.0.0.1:PORT
│
▼
exec claude ──HTTP──► claude-ds-proxy.py ──HTTPS──► DeepSeek /anthropic
├─ inject reasoning_effort
├─ strip thinking block
└─ stream response back
The Bash script is structured as four blocks: (1) arg parsing and --help,
(2) the embedded reusable secretref library, (3) config load + spawn
logic, (4) exec claude. The proxy is only spawned when at least one
effort spec is non-empty/non-off; otherwise the wrapper points
ANTHROPIC_BASE_URL straight at DeepSeek and skips the Python child.
The reusable secret-reference resolver lives between # BEGIN secretref
and # END secretref markers in claude-ds. It is intentionally
copy-paste-friendly: drop the block into another wrapper, set
SECRETREF_KEYCHAIN_SERVICE and SECRETREF_LOG_PREFIX, and you have the
same op:// / system:// / infisical:// plumbing for free.
# Spawn against a fake upstream and probe with curl
python3 -c "
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class H(BaseHTTPRequestHandler):
def log_message(*a, **k): pass
def do_POST(self):
n = int(self.headers.get('Content-Length','0'))
body = self.rfile.read(n)
self.send_response(200); self.end_headers(); self.wfile.write(body)
import sys; s = ThreadingHTTPServer(('127.0.0.1', 0), H)
print(s.server_address[1], flush=True); s.serve_forever()
" &
UP=$!
sleep 0.2
UPORT=$(lsof -p $UP -a -iTCP -sTCP:LISTEN -P -n | awk 'NR==2{split($9,a,":"); print a[2]}')
UPSTREAM_BASE_URL="http://127.0.0.1:$UPORT" \
EFFORT_DEFAULT=auto \
EFFORT_MAP="claude-opus-4-7=high" \
PROXY_DEBUG=1 \
python3 claude-ds-proxy.pygit clone https://github.com/earchibald/claude-ds.git
cd claude-ds
./claude-ds --versionThe wrapper resolves the proxy script via dirname of BASH_SOURCE[0]
(after symlink resolution), so running it directly from the source tree
works without installation.
PRs welcome against earchibald/claude-ds.
Please:
- For user-visible behaviour changes, note them in the PR description.
- For Bash changes: keep
set -euo pipefailsemantics intact; keep thesecretrefblock self-contained (no external function calls); if you touch thetmuxbranding block, verify cleanup still runs in both single-pane and multi-pane windows. - For Python changes: stdlib only. The proxy must remain a single file with no install step.
claude-ds uses SemVer. The version is the
VERSION="X.Y.Z" constant near the top of the wrapper; bump it when you
release. The CHANGELOG follows
Keep a Changelog.
MIT. See the LICENSE file for details.