@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

Packages

Package details

extension

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.
  • stallstallKillMs defaults to 0 (disabled): inactivity normally only emits a non-blocking alert. When stallKillMs > 0, the worker is killed after producing no output for that duration.
  • stopped — terminated via stop_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:

  1. Idempotent completion funnelclose, spawn error, and both watchdogs converge on one hook; a force-finalize fallback fires even if a truly wedged child never reports a clean exit.
  2. Wall-clock + stall watchdogs in the extension, with SIGTERM→SIGKILL escalation against the whole process group (claude grandchild included, not just the runner shell).
  3. Runner-level --timeout backstop (via timeout/gtimeout when 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 coreutils timeout is absent (e.g. stock macOS).
  4. Non-disableable 6-hour absolute hard-kill ceiling. timeoutMs is clamped server-side to [30_000, 21_600_000] ms (30s–6h) before the wall timer and runner --timeout are armed. There is no way to pass 0 or an oversized value to disable or extend past this ceiling — even a caller that bypasses the tool schema is clamped in spawn_claude_code.execute.
  5. Max active runs (3, hard-coded). spawn_claude_code refuses 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 a claude-code-complete hook; a claude-code-inactivity hook fires if it goes quiet. Params: prompt, cwd, tools, allowedTools, disallowedTools, model, fallbackModel, appendSystemPrompt, addDir, permissionMode, bare, maxTurns, timeoutMs, inactivityAlertMs, stallKillMs.
    • tools — optional Claude --tools expression that restricts which built-in tools are available, for example Read,Edit or an empty string to disable tools.
    • allowedTools — optional Claude allowed-tools expression that pre-approves matching calls. This is not a sandbox boundary; use tools / disallowedTools to 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 passing 0.
    • bare — rejected at runtime when true (see "No-silent-hang guarantee" above). Do not pass bare: 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 (activeOnly to filter), for monitoring a fleet of workers.
  • stop_claude_code_run — SIGTERM (escalating to SIGKILL) a running worker; a stopped completion 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 allowedTools to the narrowest useful set. A broad Bash permission 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 resolved cwd argument.
  • 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 to SIGKILL) to the process group of every worker this session owns. Remove the package with pi remove npm:@mporenta/pi-claude-code.

Troubleshooting

  • Runner not found: confirm scripts/claude_cli_runner.sh shipped and is executable (ls -l scripts/).
  • Missing dependency: the runner exits 2 if claude or jq is not on PATH.
  • Non-zero exit / error subtype: inspect the artifact with get_claude_code_run; the runner status envelope reports status, subtype, exit_code, and artifact paths. See references/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.