claude-sub
v0.4.0
Published
PATH shim that routes claude -p invocations through interactive Claude when CLAUDE_USE_SUB is set
Readme
claude-sub
A PATH shim that routes claude -p calls through an interactive Claude session so they bill against your Claude subscription instead of API credits.
Disclaimer. Using
claude-submay violate the Anthropic terms of service governing your Claude subscription — review them and the Claude Code documentation before relying on it.
claude-sub installs ahead of the real claude binary on your PATH. When enabled, it intercepts -p / --print invocations, spawns interactive Claude under a PTY, sends the prompt as keystrokes, waits for the reply, extracts the clean text, and writes it to stdout. Every other invocation passes straight through. Toggle it on or off with csub on / csub off — no environment variables required.
Install
npx claude-sub setupsetup detects your shell (zsh / bash / fish), shows you the one-line PATH edit it plans to make, asks for confirmation, writes the line with a marker comment so uninstall can reverse it, and verifies that which claude resolves to the shim. The shim ships disabled — your existing claude calls keep working until you run csub on.
To update later:
npx claude-sub@latest setupThe @latest matters: plain npx claude-sub reuses npx's cached copy without checking the registry. setup is idempotent — when the marker line already points at the new install it makes no changes, and when it points at an old location (e.g. a previous npx cache dir) it shows a diff and repoints it in place.
To remove it later:
npx claude-sub uninstallThis strips the marker line from your rc file and uninstalls the global package.
Usage
csub on # route `claude -p` through your subscription
csub off # passthrough only; `claude -p` hits the API again
csub status # show whether routing is on or off, and which `claude` PATH wins
csub --version # print the installed version (also: csub version, csub -v)Once csub on is set, just use claude as you normally would:
claude -p "reply with the single word OK"
# OKExit codes: 0 on success, 1 for unsupported flags or when the session ends without a clean reply, 124 if the PTY session times out. Failures write a csub:-prefixed diagnostic to stderr — the shim never passes raw terminal output off as a reply.
Supported flags
When routing is enabled, the following flags are forwarded to the interactive session:
| Flag | Description |
|------|-------------|
| --model / -m | Model to use (e.g. claude-sonnet-4-5) |
| --verbose / -v | Enable verbose output |
| --append-system-prompt | Append to the system prompt |
| --system-prompt | Override the system prompt |
| --permission-mode | Set permission mode (e.g. acceptEdits) |
| --dangerously-skip-permissions | Skip permission prompts |
| --settings | Path to a settings file |
| --agent / --agents | Agent configuration |
| --strict-mcp-config | Strict MCP config validation |
| --bare | Bare-mode output |
| --add-dir | Additional working directories (variadic) |
| --mcp-config | MCP config paths (variadic) |
| --allowedTools / --allowed-tools | Tool allowlist (variadic) |
| --disallowedTools / --disallowed-tools | Tool denylist (variadic) |
| --tools | Tool list (variadic) |
| --plugin-dir | Plugin directories (variadic) |
| --output-format stream-json | Emit a Claude-compatible NDJSON event stream — consumed by the shim, not forwarded (see JSON output) |
| --output-format json | Emit a single JSON result object on completion — consumed by the shim, not forwarded (see JSON output) |
Flag and prompt order don't matter: claude -p --output-format stream-json "question" and claude -p "question" --output-format stream-json both send "question" as the prompt. Any other flag causes the shim to exit non-zero with a csub:-prefixed message naming the unsupported flag and listing what's accepted.
JSON output
Both upstream JSON output formats are emulated, not rejected. With csub on the call routes through your subscription like any other -p invocation — no "will bill against API" warning. The PTY session is still plain text internally; the JSON shapes are synthesized around it. Both the space-separated (--output-format json) and equals (--output-format=json) forms work.
Single-object JSON (--output-format json)
The shim stays silent during the run and prints exactly one JSON object on completion:
claude -p "reply with the single word OK" --output-format json
# {"type":"result","result":"OK"}
claude -p "reply with the single word OK" --output-format json | jq -r .result
# OKThe envelope is minimal by design — no cost_usd, usage, or session_id fields, since the shim does not have upstream's metadata.
Streaming JSON (--output-format stream-json)
The shim emits a Claude-compatible NDJSON (newline-delimited JSON) event stream on stdout.
claude -p --output-format stream-json "reply with the single word OK"
# {"type":"heartbeat"}
# {"type":"assistant","message":{"content":[{"type":"text","text":"OK"}]}}
# {"type":"result","result":"OK"}Events emitted:
heartbeat—{"type":"heartbeat"}, written during the run at most once every ~10s, and only when the PTY produced new output since the previous interval. It is a genuine proof-of-life: when the interactive session stalls, the stream goes quiet, so a watcher can tell "still working" from "wedged." Nothing acts on the heartbeat automatically — it is purely observational.assistant—{"type":"assistant","message":{"content":[{"type":"text","text":"…"}]}}, written once at the end, carrying the full clean reply.result—{"type":"result","result":"…"}, the terminal event carrying the same clean, sentinel-stripped text the plain-text path produces.
These shapes satisfy the jq filters a stream-json consumer (e.g. ralph.sh) uses: .type=="assistant" | .message.content[] | select(.type=="text").text for live text and .type=="result" | .result for the outcome. This makes claude-sub a drop-in for tools that pipe --output-format stream-json through jq.
Only stream-json and json are supported. Other --output-format values (e.g. text) exit non-zero with a message; on failure (timeout, idle, or no clean reply) the shim writes a diagnostic to stderr and exits non-zero, as in plain mode.
Running under a sandbox (srt)
claude-sub is designed to run inside Anthropic's sandbox-runtime (srt) — e.g. when ralph.sh wraps each iteration's claude call. Because the shim drives interactive Claude through a pseudo-terminal, the sandbox must grant pty access. The default sandbox profile denies /dev/ptmx and pty slave devices, so node-pty fails with posix_spawnp failed / PTY error.
Add allowPty: true to your srt settings file (alongside filesystem and network):
{
"allowPty": true,
"filesystem": { "...": "..." },
"network": { "allowedDomains": ["*.anthropic.com", "anthropic.com"], "...": "..." }
}The sandbox also needs write access to wherever interactive Claude persists session state — typically ~/.claude and ~/.claude.json — plus the usual temp directories. With those granted and allowPty: true, the shim's pty session spawns under the sandbox and stream-json routes through your subscription (no API-bypass warning).
Known limitations
The following flags and features are not supported when routing is on:
--output-format text— onlystream-jsonandjsonare emulated (see JSON output)--resume— session resume requires API session IDs not available under a PTY--json/--stream-json— these are not real upstreamclaudeflags; rejected like any other unsupported flag, with the supported list (including both--output-formatvalues) in the message--no-markdown— not forwarded; output formatting follows interactive Claude defaults- Piped stdin — the PTY session cannot receive stdin piped from a shell pipe
- Non-print invocations — only
-p/--printroutes through your subscription; everything else passes through to the real binary
Timeouts
| Variable | Default | Description |
|----------|---------|-------------|
| CLAUDE_USE_SUB_TIMEOUT_MS | 300000 (5 min) | Overall timeout for a routed invocation |
If the session times out, the shim exits with code 124 and writes a diagnostic to stderr containing the elapsed time and the last 4 KB of raw PTY output.
Troubleshooting
Run csub doctor first — it reports whether the shim is in front of the real claude binary and prints a remediation hint if it isn't.
which claude resolves to the real binary. Your global npm bin directory isn't ahead of the real claude on your PATH. npx claude-sub setup adds the right line for your shell; if you ran it before installing claude itself, run it again, or open a fresh shell so the rc-file change takes effect.
Manual install (without setup). If you'd rather wire the PATH yourself:
npm install -g claude-sub
# then, in your shell profile:
export PATH="$(npm bin -g):$PATH"After that, which -a claude should list the shim first and the real claude second.
Install from a local tarball.
pnpm pack # produces claude-sub-<version>.tgz
npm install -g ./claude-sub-<version>.tgz