@saccolabs/pi-claude-cli
Pi coding agent extension that routes LLM calls through the Claude Code CLI
Package details
Install @saccolabs/pi-claude-cli from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@saccolabs/pi-claude-cli- Package
@saccolabs/pi-claude-cli- Version
0.8.2- Published
- Sep 14, 2026
- Downloads
- 4,320/mo · 773/wk
- Author
- saccoagustin
- License
- MIT
- Types
- extension
- Size
- 257.7 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-claude-cli
This is a maintenance fork. The upstream project's last commit was in March 2026, and it had stopped working against current pi. This fork updates it for compatibility with the current pi version, and folds in three open upstream pull requests that hadn't been merged:
- #25 — don't resume a Claude CLI session that was never created (fixes empty replies when switching to this provider mid-conversation), and surface CLI errors instead of silently returning nothing.
- #26 — fix a false "not authenticated" warning on Claude Code 2.x, and correct the outdated login instructions.
- #29 — let all models use the full thinking-effort range (up to
max), not just Opus.Together these resolve the widely-reported problem where prompting a
pi-claude-climodel just returned an empty response (#3). Credit for the three fixes goes to their original PR authors.
A pi extension that routes LLM calls through the Claude Code CLI as a subprocess. Use your Claude Pro/Max subscription as the LLM backend — no API key, no separate billing.
Best experienced in pidex
This fork is developed against pidex — the pi coding agent extended into a desktop IDE, and the most advanced multi-provider agentic IDE you can run on your own machine. pidex works with every pi provider (Anthropic, OpenAI, Google, Bedrock, and the rest), and with this extension it turns a Claude Pro/Max subscription into a full desktop IDE: chat with real diffs, file explorer, terminal, and versioned artifacts — no API key needed.

Everything this extension emits has a first-class surface there:
- The
[Claude Code · Tool]activity markers render as expandable steps in the transcript, not raw text. - The
claude-rate-limitstatus key feeds pidex's context meter, so your plan's usage window and reset time are always visible. - The
claude-subagentsstatus key turns aTaskfan-out into live per-agent progress instead of a blank pane. - pidex sets
PI_CLAUDE_CLI_STRICT_MCP=1on every session, so the CLI's MCP traffic stays under the host's tool guards.
How it works
The extension registers as a custom pi provider exposing all Claude models. It runs in observer mode: the Claude Code CLI is a first-class agent that owns its loop, its tools and its session — pi is the system of record and observes the stream. One CLI session per pi session, resumed with --resume on every follow-up turn, so token use matches using the CLI directly. Built-in tools (Read, Bash, …) execute natively inside the CLI and surface to pi as [Claude Code · Name] activity markers. Custom pi tools are advertised via a schema-only MCP server and handed off: the provider interrupts the turn cleanly, pi executes the tool (all pi hooks fire), and the next turn resumes with the result.
Requirements
- Claude Code CLI installed and authenticated (
claudeon PATH) - A Claude Pro or Max subscription
- pi or GSD
Installation
pi install npm:@saccolabs/pi-claude-cli
Or declare it in ~/.pi/agent/settings.json (global) or .pi/settings.json (project):
{
"packages": ["npm:@saccolabs/pi-claude-cli"]
}
Then select a Claude model via /model in the interactive UI. All Claude models appear under the pi-claude-cli provider.
Requires the claude binary on your login-shell PATH (npm install -g @anthropic-ai/claude-code), authenticated with your Claude Pro/Max account.
Features
- Streams text, thinking, and tool call tokens in real-time
- Maps tool names and arguments bidirectionally between Claude and pi
- Exposes custom pi tools to Claude via MCP; the schema server proxies each call back to pi over a local socket, so the CLI never has to be interrupted to let pi run a tool
- One CLI process per pi session (0.7.0): it lives through custom-tool handoffs and across turns, so Claude Code builds its system prompt — git snapshot included — once per session rather than once per call. A commit or branch rename between turns no longer re-bills the whole context
- One CLI session per pi session (sidecar-mapped), resumed on every follow-up turn — native caching, no history replay
- Native tool execution: the CLI runs its own tools; guards are injected as Claude Code PreToolUse hooks via
PI_CLAUDE_CLI_SETTINGS - Reports account rate-limit state (window, reset, overage) to the front-end
on the
claude-rate-limitstatus key — never mixed into turn content - Surfaces sub-agent fan-outs: one marker when a
Taskagent starts and one when it reports, plus live per-agent progress on theclaude-subagentsstatus key — so a fan-out is no longer a blank pane - Background sub-agents get to finish: a
resultwhile agents are still running ends a cycle, not the turn, so their reports reach the model instead of dying with the subprocess - Configurable thinking effort across the full ladder (low to max), mapped 1:1 for every model: the level the host asks for is the level the CLI gets
- Cross-platform subprocess management (Windows, macOS, Linux)
- Inactivity timeout and process registry for cleanup
Architecture
docs/ARCHITECTURE.md covers the turn lifecycle, the three-way tool split,
the two-ledger session model, error recovery, and the CLI compatibility
notes (including the 2.x control-protocol shape).
Two of its sections are contracts a front-end can depend on, so read
them before changing what this extension emits: the
[Claude Code · Tool {args}] marker string, and the claude-rate-limit
status key.
What your Claude environment contributes
By default a real claude -p process lives across turns in your workspace,
so your Claude Code environment participates through three doors:
- Tools — the six built-ins (Read/Write/Edit/Bash/Grep/Glob) execute natively in Claude Code. Only custom pi tools become pi tool calls.
- CLI-side execution — your personal/project MCP servers, WebSearch,
and sub-agents run inside the CLI between cycles. They appear in the
transcript as one-line markers (
[Claude Code · WebSearch {…}]) and bill your plan. - Prompt-level osmosis — the CLI auto-loads project CLAUDE.md and
memory, your hooks fire, and skills can load twice (natively via
claude, and again via pi's own
~/.claude/skillssupport).
Pi context with native tools (0.7.1+)
PI_CLAUDE_CLI_CONTEXT=pi makes pi the source for project instructions,
skills and custom integrations, while retaining Claude Code's default prompt
and native tools. The generated pi tool guidance is aligned to native schemas;
artifact guidance, user instructions and .pi paths are preserved. Duplicate
Claude discovery is disabled, but explicit host guards remain enabled.
Requires Claude Code 2.1.263+. Hosts must retain pi context-file discovery
(no --no-context-files) and start fresh sessions when changing policies. Do not combine this with system-prompt
replacement or bare mode. See the context-policy contract
for launch controls, limitations, and the opt-in live verification.
Hermetic mode
Set PI_CLAUDE_CLI_HERMETIC=1 to keep that environment out of pi turns:
the subprocess runs with --strict-mcp-config (only this extension's
schema-only custom-tools server loads) and an empty --setting-sources
(no user/project/local settings — hooks, auto-memory, permission
allowlists). Model access and your subscription login are unaffected.
Strict MCP mode
PI_CLAUDE_CLI_STRICT_MCP=1 passes --strict-mcp-config on its own, with
no --setting-sources blackout. Use it to route every MCP call through pi's
own tool registry — typically pi-mcp-adapter's mcp gateway — while leaving
the CLI's settings, hooks and CLAUDE.md auto-memory alone.
Reach for this instead of hermetic mode when the host suppresses pi's copy of
CLAUDE.md and relies on the CLI to load it: hermetic mode would leave the
model with project instructions from neither side. Hermetic mode still implies
strict MCP, so setting both is safe.
Why a host wants it: MCP servers the host did not configure are invisible to it, bypass its tool guards, and are never counted by pi-side status or context accounting.
Related knobs: PI_CLAUDE_CLI_TIMEOUT_MS overrides the 300s inactivity
timeout (CLI-side tools can be silent on stdout for minutes).
Tool result forwarding
In observer mode the CLI executes its own tools, and pi's transcript records
each one as a marker text block — [Claude Code · Bash {"command":…}] — with
no result. A front-end can show what was invoked but never what came back,
so its tool rows have nothing to expand into.
PI_CLAUDE_CLI_TOOL_RESULTS=1 forwards the results. Two richer marker shapes
go on the wire, paired by tool_use_id:
[Claude Code · <ToolName> #<toolUseId> <argsJson>] ← call, now id-tagged
[Claude Code · result #<toolUseId> <payloadJson>] ← its result
payloadJson is complete, parseable JSON. Every payload carries
status ("ok" / "error"), the tool it answers, a printable summary,
the preview (capped at 2,000 characters) and the full length — plus
whatever metrics the tool's own result made available:
| Tool | Metrics | summary |
|---|---|---|
Read |
path, lines, totalLines |
419 lines / lines 201-300 of 900 |
Bash |
lines, bytes, stderrLines |
4 lines out · 2 on stderr |
Bash (failed) |
exitCode, error |
exit 1 · ls: /nope: No such file or directory |
Edit / Write |
path, added, removed, lines |
+1 -1 in poem.txt / created poem.txt |
Grep |
files, matches |
1 match / 12 matches in 3 files |
Glob |
files, durationMs, filesTruncated |
4 files |
| anything else | lines |
12 lines / empty |
These come from the CLI's own tool_use_result object, which it publishes on
the user envelope beside each tool_result. A tool whose shape this
provider does not recognise still gets status, summary and a line count,
so a row is never reduced to "ok". The full output stays in the CLI's own
transcript.
Results are only forwarded for tools that produced a call marker — handoff
tools are executed by pi, which already has their real result, and their
replayed tool_result envelopes are ignored.
This is a host opt-in because it changes the marker wire contract: a
front-end that has not learned the id-tagged shapes would render them as
prose. Leave it unset and the call marker keeps its pre-0.6.0 shape (0.8.0
still improves what is inside argsJson — see below).
What a call marker contains
argsJson is complete, parseable JSON as of 0.8.0. It used to be
JSON.stringify(input) cut at 120 characters, which meant it usually did not
parse, and the cut took the END off values — so Read of a deep worktree
path rendered as Read cl… while the marker spent its whole budget on
directory names. src/tool-markers.ts now picks each tool's identifying
arguments in priority order, clips values one at a time (paths from the
front, so the filename survives), replaces bulk arguments with measurements
rather than dumping them, and enforces its 700-character budget by dropping
trailing fields instead of cutting the document:
[Claude Code · Read {"file_path":"…/features/chat/items/transcriptRows.ts"}]
[Claude Code · Write {"file_path":"/repo/poem.txt","lines":3,"bytes":70}]
[Claude Code · Edit {"file_path":"/repo/a.ts","new_lines":3,"old_lines":2}]
[Claude Code · TodoWrite {"todos":4,"done":2,"active":"Wiring the marker"}]
Persistent CLI process
Before 0.7.0 every pi call was its own claude -p process: a custom-tool
handoff denied the permission, interrupted the CLI, ran the tool in pi and
--resumed a new process with the result pasted in as text; every user turn
started another. Each new process rebuilt Claude Code's system prompt, and
that prompt snapshots git status, the recent commits and the branch. So a
commit, a branch rename or a new untracked file between two processes
re-billed the entire context as cache write — measured 2026-09-01 on one
session: 64k, 106k and 190k tokens on three separate restarts, 1.87M tokens
across three sessions that day. The restart also left tool use was rejected / [Request interrupted] / No response requested. filler in the
CLI transcript on every custom tool call.
Now the process stays up. A custom tool call is allowed and proxied: the
schema-only MCP server forwards tools/call to pi over a local socket, pi
runs the tool, and the next pi call answers the CLI on the same process — its
transcript records a real tool_result. After a turn ends the process is
parked and the next user message goes to the same stdin. Measured live
(tests/live-persistent.test.ts): a turn after a commit costs 91 cache-write
tokens on the persistent process versus 8,827 on a fresh one.
Reaching that socket means being able to make pi run a tool, so since 0.8.2 it
is private. The socket, the tool schemas, every --mcp-config and every staged
system prompt live in a randomly-named 0700 per-process directory and are
written 0600; each request carries a per-process secret, which the schema
server reads from a file rather than taking on its command line (argv is
world-readable). Before that they were pid-named files in a shared /tmp with
default permissions — see
the runtime directory.
| Variable | Default | Meaning |
|---|---|---|
PI_CLAUDE_CLI_KEEPALIVE_MS |
600000 |
How long a parked process waits for the next turn. 0/off: end it at result as before (handoffs still keep it alive within a turn). |
PI_CLAUDE_CLI_HANDOFF_WAIT_MS |
1800000 |
Ceiling on a process blocked in a handoff that pi never answers (aborted tool, crashed host); it is interrupted and retired when it fires. |
PI_CLAUDE_CLI_HANDOFF_PROXY |
on | 0 restores interrupt-and-resume for custom tools (the MCP server then answers tools/call with an error result). |
MCP_TOOL_TIMEOUT |
3600000 |
Passed to the CLI when unset: a proxied call blocks until pi has run the tool, and sub-agents take minutes. |
A parked process is retired — cleanly, never mid-turn — when the next call
does not match it: a different model or effort, a changed system-prompt
mode, a rewritten tool schema (a new MCP server connected), a pi history the
CLI never saw, or a delta that is not exactly the awaited tool results. The
next call then --resumes the CLI session in a fresh process, exactly as
every call did before. Ending pi ends its parked processes: their stdin is a
pipe from pi, and the CLI exits on EOF.
Auto-compact window
The provider resumes one CLI session for a pi session's whole life, and
nothing else ever shrinks it. On 1M-context models the CLI's own auto-compact
default lets that session ratchet toward a million tokens — measured across 26
real sessions, contexts reached 480k+, the average request carried 202k
tokens, and every request re-reads the full context. So the provider passes
--autocompact 200000 by default: Claude Code compacts the session itself
when its context nears 200k, keeping the cached system-prompt prefix and full
transcript fidelity.
PI_CLAUDE_CLI_AUTOCOMPACT configures it (read per spawn, like the flags
above):
| Value | Behaviour |
|---|---|
| (unset) | --autocompact 200000 — the 200k budget these models run under everywhere the 1M beta is off. |
400k, 400000, 400 |
Any window from 100k to 1M; bare numbers are thousands (CLI shorthand). |
auto |
--autocompact auto — the CLI's own default (≈ the model's full window). |
off |
Omit the flag entirely (use on CLIs that predate --autocompact). |
The value is a token count, not a percentage: cache read/write bill per token and every request re-reads the whole context, so the sane budget is the same on a 200k model and a 1M one. Invalid values warn and fall back to the default instead of reaching the CLI, which rejects them by refusing to start.
Note for pre-existing sessions: the first resumed turn of a session already past the window compacts immediately — one summarization pass, then the session continues small. That is the remediation, not a bug. pi's own compaction is separate (it rewrites pi's transcript, never the CLI session's) and with this cap it should rarely trigger.
Which system prompt
PI_CLAUDE_CLI_SYSTEM_PROMPT chooses whose system prompt the subprocess
runs under. It is read per spawn, so a host can change it between sessions.
| Value | Behaviour |
|---|---|
claude (default) |
--append-system-prompt-file: pi's prompt layers on top of Claude Code's own. |
pi |
--system-prompt-file: pi's prompt replaces Claude Code's entirely. |
The -file suffix matters: --system-prompt / --append-system-prompt
(unsuffixed) take a literal string, not a path. Passing a temp-file path
to the unsuffixed flag makes the path itself the prompt — pi's instructions
never reach the model, silently, with no error. This shipped unnoticed since
the provider's system-prompt support was first added; see the correction
below.
minimal is accepted as an alias for pi, append for claude; anything
unrecognised falls back to the default rather than failing a session.
Why you might want pi. The point of a minimal harness is not inheriting
another agent's preamble. Measured on a real session, the CLI's fixed cached
prefix was 17,475 tokens; the tool schemas (~4.3k) stay either way, but the
rest is Claude Code's prompt, and pi's own — after the tool-section rewrite
below — is ~674 tokens. That frees roughly 12k tokens of context window per
call. It is a window win, not a cost win: the prefix is cached and bills at
0.1x.
Why the default is still claude. Claude Code's prompt carries operating
guidance for its own tools. Replacing it leaves the model with pi's
instructions plus the raw tool schemas. To stop that being actively
misleading, pi mode rewrites pi's tool sections — which name pi's tools
(read, edit, grep, find, ls) and pi's parameters (path,
oldText, newText) — into Claude Code's vocabulary (Read, Edit,
Grep, Glob, with file_path, old_string, new_string). If pi ever
restyles its prompt so the Available tools: / Guidelines: anchors are
missing, the prompt passes through untouched rather than being mangled.
The system prompt goes on every spawn, not just the session-creating one:
the CLI does not keep --system-prompt-file across --resume, and a resumed
session without it silently reverts to Claude Code's default prompt from turn
2 onwards. Because an identical prefix is what keeps the prompt cache warm,
the prompt a session was created with is stored in the sidecar
(~/.pi/agent/pi-claude-cli/sysprompt/<cli-session-id>.txt) and replayed
verbatim rather than rebuilt. A change to the mode therefore takes effect on
the next new session, not the current one.
Correction (2026-08-29). Both bullets above named the unsuffixed flags until this date. They were wrong the whole time the provider has supported a system prompt:
--system-prompt/--append-system-prompttake a literal string, and the provider was handing them a temp-file path. That path string either became the entire "system prompt" (pimode) or got appended as noise Claude Code's model ignored (claudemode) — either way, pi's actual instructions never reached the model, on ANY turn, since the very first spawn. Fixed by switching to--system-prompt-file/--append-system-prompt-file, which take a path. See pidex's write-up for the live before/after.
License
MIT