# 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` — `stallKillMs` 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. Before signalling the worker, it writes a terminal ledger at `/tmp/pi-claude-code/<run-id>/run.json`; a replacement extension reloads that record, and `get_claude_code_run` / `list_claude_code_runs` expose it with `recovered: true`.

Enforcement layers:

1. **Idempotent completion funnel** — `close`, 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, otherwise a bundled Node watchdog) so a wedged worker still self-terminates even if the Pi process dies and orphans it, including on 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

```bash
pi install npm:@mporenta/pi-claude-code
```

Project-scoped install (writes to `.pi/settings.json` so a team shares it):

```bash
pi install -l npm:@mporenta/pi-claude-code
```

Temporary trial without installing:

```bash
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 <id>.

## 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 additional Claude deny rules. Every worker is also launched with mandatory `--disallowedTools "Agent,Task"`, so nested-agent capabilities cannot be enabled by prompt or omitted caller configuration.
  - `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. It also finds durable `shutdown` records from a prior session and marks them `recovered: true`.
- `list_claude_code_runs` — enumerates every run in the session with liveness and termination reason (`activeOnly` to filter), plus recoverable shutdown records from prior sessions.
- `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:

```json
{
  "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:

- **Nested agents:** every worker hard-denies Claude Code `Agent` and `Task` tools at the CLI layer. This is independent of prompt wording and cannot be overridden by the caller.
- **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 durably records `terminationReason: "shutdown"`, then sends `SIGTERM` (escalating to `SIGKILL`) to the process group of every worker this session owns. The record intentionally replaces a teardown-time completion hook and remains discoverable after restart. 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

```bash
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):

```bash
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

```bash
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`.
