@mporenta/pi-claude-code
Pi extension that runs Claude Code headlessly through an agent-safe CLI runner with a no-silent-hang guarantee (wall-clock + stall watchdogs, process-group kill, guaranteed completion hook), injects a completion hook back into Pi, and bundles a delegation
Package details
Install @mporenta/pi-claude-code from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@mporenta/pi-claude-code- Package
@mporenta/pi-claude-code- Version
0.2.1- Published
- Jul 18, 2026
- Downloads
- 80/mo · 80/wk
- Author
- pi-porenta
- License
- MIT
- Types
- extension
- Size
- 97.9 KB
- Dependencies
- 0 dependencies · 5 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 Code Extension
@mporenta/pi-claude-code runs Claude Code headlessly from Pi through an agent-safe CLI runner. It spawns Claude as a normal child process, keeps the (potentially large) result out of Pi's context by writing it to a disk artifact, and injects a claude-code-complete message back into Pi when the worker exits. That message uses triggerTurn: true with deliverAs: "steer": while Pi is active, the completion enters at the next safe turn boundary so relevant evidence can be incorporated before the main response; while Pi is idle, it starts a turn immediately.
No cmux dependency is used. A bundled claude-code skill teaches Pi to delegate through the extension tools instead of shelling out to claude -p directly.
No-silent-hang guarantee
The orchestrating Pi agent can never be left waiting on a worker that quietly wedged. Every spawned run reaches exactly one claude-code-complete hook within a bounded time, tagged with a terminationReason:
completed— Claude exited cleanly.claude_error— Claude exited non-zero or reported an error subtype.timeout— exceeded the wall-clock ceiling (timeoutMs, default 30 min, clamped to a non-disableable 30s–6h range); killed.stall—stallKillMsdefaults to0(disabled): inactivity normally only emits a non-blocking alert. WhenstallKillMs > 0, the worker is killed after producing no output for that duration.stopped— terminated viastop_claude_code_run.spawn_error— the worker never started.shutdown— Pi tore down the session while the worker was still running.
Normal terminal paths (completed, claude_error, timeout, stall, stopped, spawn_error) steer a claude-code-complete hook into the active run, or trigger a turn immediately when Pi is idle. This prevents worker completions from accumulating as separate follow-up turns after the main agent stops. shutdown is the exception: session_shutdown suppresses hook injection during teardown, since Pi is already exiting and cannot receive it.
Enforcement layers:
- Idempotent completion funnel —
close, spawnerror, and both watchdogs converge on one hook; a force-finalize fallback fires even if a truly wedged child never reports a clean exit. - Wall-clock + stall watchdogs in the extension, with SIGTERM→SIGKILL escalation against the whole process group (
claudegrandchild included, not just the runner shell). - Runner-level
--timeoutbackstop (viatimeout/gtimeoutwhen present) so a wedged worker still self-terminates even if the Pi process dies and orphans it. Degrades to a no-op with a logged note where coreutilstimeoutis absent (e.g. stock macOS). - Non-disableable 6-hour absolute hard-kill ceiling.
timeoutMsis clamped server-side to[30_000, 21_600_000]ms (30s–6h) before the wall timer and runner--timeoutare armed. There is no way to pass0or an oversized value to disable or extend past this ceiling — even a caller that bypasses the tool schema is clamped inspawn_claude_code.execute. - Max active runs (
3, hard-coded).spawn_claude_coderefuses to start a new worker while 3 runs spawned by this session are still unfinalized, so a runaway loop cannot pile up unbounded paid Claude processes. Stop or wait for a run to finish before spawning another; this limit is not configurable via env or settings.
Workers are deliberately not tied to the tool-call abort signal — they are background jobs that outlive the immediately-returning spawn call, so an ending turn can't silently kill one.
bare: true is rejected at runtime with a clear error — it bypasses hooks/plugins/MCP/CLAUDE.md and breaks the orchestration safety model described above. The bare parameter remains in the schema only so existing calls get this explicit rejection instead of a silent schema failure; do not pass bare: true.
Non-blocking inactivity alerts
Separately from termination, the orchestrator is kept aware of a quiet worker. The runner spawns Claude with --output-format stream-json --verbose --include-partial-messages and writes the JSONL event log to the artifact as it is produced, so the file grows in real time. The extension polls that file's size; if it has not grown for inactivityAlertMs (default 120000) while the worker is still running, it injects a claude-code-inactivity hook — without killing the worker — and repeats every interval that silence continues (with an escalating inactivityAlertCount).
This is an alert, not a kill: a long test run or slow build legitimately produces no events for a while, so the orchestrator is told to inspect (get_claude_code_run) and decide, rather than the worker being terminated automatically. A hard inactivity kill is available but opt-in via stallKillMs (default 0 = off); the 30-minute wall-clock timeout remains the only default hard kill.
Install
pi install npm:@mporenta/pi-claude-code
Project-scoped install (writes to .pi/settings.json so a team shares it):
pi install -l npm:@mporenta/pi-claude-code
Temporary trial without installing:
pi -e npm:@mporenta/pi-claude-code
Requires the Claude Code CLI (claude) and jq on PATH, plus existing Claude authentication. The runner never performs auth bootstrap.
First prompts to try
Use spawn_claude_code to review this repository for correctness. Do not edit files. Run read-only validation only and return actionable findings. Use allowedTools
Read,Bash(git status *),Bash(git diff *),Bash(npm test).
Use get_claude_code_run with the run ID from the last completion hook and summarize the artifact.
Use stop_claude_code_run to terminate run .
Tools
spawn_claude_code— starts a headless worker and returns immediately with a run ID. Completion arrives as aclaude-code-completehook; aclaude-code-inactivityhook fires if it goes quiet. Params:prompt,cwd,tools,allowedTools,disallowedTools,model,fallbackModel,appendSystemPrompt,addDir,permissionMode,bare,maxTurns,timeoutMs,inactivityAlertMs,stallKillMs.tools— optional Claude--toolsexpression that restricts which built-in tools are available, for exampleRead,Editor an empty string to disable tools.allowedTools— optional Claude allowed-tools expression that pre-approves matching calls. This is not a sandbox boundary; usetools/disallowedToolsto constrain availability.disallowedTools— optional Claude deny rules, passed through to--disallowed-tools, for denying specific tools/patterns.timeoutMs— wall-clock ceiling in ms; clamped to[30_000, 21_600_000](30s–6h). This ceiling cannot be disabled, including by passing0.bare— rejected at runtime whentrue(see "No-silent-hang guarantee" above). Do not passbare: true.
get_claude_code_run— returns liveness (elapsed, timeout budget remaining, ms since last output, inactivity-alert count), termination reason, and a bounded artifact excerpt when finished.list_claude_code_runs— enumerates every run in the session with liveness and termination reason (activeOnlyto filter), for monitoring a fleet of workers.stop_claude_code_run— SIGTERM (escalating to SIGKILL) a running worker; astoppedcompletion hook follows.
The runner is always invoked with an explicit workdir, a live-streamed JSONL event-log artifact at /tmp/pi-claude-code/<run-id>/result.json, and its own --timeout backstop. The completion hook and get_claude_code_run surface the final result via the status envelope's result_text. Pass maxTurns to cap agentic turns; omit it for the runner's unlimited-turn default.
Bundled skill
The extension registers its bundled claude-code skill at runtime via resources_discover, so the skill loads automatically when the package is installed — there is no separate skill registration to configure. The skill instructs Pi to use the three tools above, to never pass bare: true, and to independently verify Claude's claims.
The npm package version and bundled skill version are tracked independently: the package version describes the extension release; the skill version describes the delegation guidance bundled with it.
Configuration
Narrow which resources load from the package in global (~/.pi/agent/settings.json) or project (.pi/settings.json) settings:
{
"packages": [
{ "source": "npm:@mporenta/pi-claude-code" }
]
}
Per-call configuration is passed as tool arguments: prompt, cwd, tools, allowedTools, disallowedTools, model, fallbackModel, appendSystemPrompt, addDir, permissionMode, bare, maxTurns, timeoutMs, inactivityAlertMs, and stallKillMs.
Security
The Claude process inherits Pi's environment and filesystem permissions. Notes:
- Shell scope: restrict
allowedToolsto the narrowest useful set. A broadBashpermission is not read-only. - Paths: the runner writes artifacts under
${TMPDIR:-/tmp}/claude-cli-runner/and the extension under/tmp/pi-claude-code/<run-id>/. Working directory is the resolvedcwdargument. - Environment: the child inherits
process.env, including any credentials present in Pi's environment. - Network: the Claude CLI contacts Anthropic's API using existing local auth.
- Cleanup / disable: session shutdown sends
SIGTERM(escalating toSIGKILL) to the process group of every worker this session owns. Remove the package withpi remove npm:@mporenta/pi-claude-code.
Troubleshooting
- Runner not found: confirm
scripts/claude_cli_runner.shshipped and is executable (ls -l scripts/). - Missing dependency: the runner exits
2ifclaudeorjqis not onPATH. - Non-zero exit / error subtype: inspect the artifact with
get_claude_code_run; the runner status envelope reportsstatus,subtype,exit_code, and artifact paths. Seereferences/claude-cli-runner-status-contract.md.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # bundled runner regression (fake claude, no network/auth)
npm run pack:dry-run # inspect the publishable tarball file list
Local Pi smoke test (loads the extension and its bundled skill without launching a Claude worker):
pi --no-session -p "Reply with exactly OK" -e ./index.ts
Reload after editing when installed for auto-discovery: restart Pi or run /reload inside a session.
Publish
npm run typecheck && npm test && npm run pack:dry-run
npm publish --access public
Then install anywhere with pi install npm:@mporenta/pi-claude-code.
License
MIT — see LICENSE.