predexec
Predictive execution: collapse an adaptive multi-level tool sequence into a single model round-trip via a deterministic plan tree. pi coding agent extension / opencode plugin / Claude Code MCP server.
Package details
Install predexec from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:predexec- Package
predexec- Version
0.4.1- Published
- Aug 31, 2026
- Downloads
- 604/mo · 42/wk
- Author
- furiouszen
- License
- MIT
- Types
- extension, skill
- Size
- 368.6 KB
- Dependencies
- 2 dependencies · 0 peers
Pi manifest JSON
{
"extensions": [
"./dist/.pi/extension/index.js"
],
"skills": [
"./.pi/skills"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
predexec
Predictive execution for LLM coding agents. predexec collapses an adaptive, multi-level tool sequence into a single model round-trip: the model pre-compiles its branch decisions into a tree of deterministic predicates, and an engine walks the tree with no model call between levels. On a request-limited free provider this trades abundant tokens for scarce provider requests.
This package ships four adapters, each registering one tool, predexec:
a pi coding agent extension,
an opencode plugin, a
Claude Code MCP server, and a
Codex CLI MCP server — the same stdio server as Claude
Code's, started with --host codex to select Codex's policy reader and stats label.
See How it works below for the design and current status.
Status: read-only. The pure-TS core and all four adapters are done and unit-tested. predexec speculates read-only only — any write/install/delete hard-stops before running.
How it works
The model fills in a plan tree: each node runs a batch of shell commands and/or read-only
tool calls (read/grep/find/ls); each edge is a machine-evaluable condition on that
node's output. After running a node, the engine
evaluates outgoing edges in order, follows the first match to a child, and repeats — with no
model in the loop. It stops and returns a transcript when it reaches:
| stop | meaning |
|---|---|
leaf |
no edges — success path complete (the only non-fallback stop) |
noEdgeMatch |
no edge matched — benign miss, agent resumes normally |
maxDepth |
depth cap hit |
mutationStop |
next node writes/installs/deletes — hard stop before any mutation |
error |
invalid plan (returned gracefully, never thrown) |
aborted |
abort signal |
Adaptive depth. Plan as deep as you can confidently predict each branch. A tree of one node with no edges is valid and expected — that's just running a command (depth 0). Depth scales up only when branches are genuinely predictable.
Condition DSL (confidence-tiered)
HIGH-confidence (may gate deeper speculation): exitCode, fileExists, jsonPath, numeric,
always. LOW-confidence (may branch only to a read-only node): match (regex over stdout/stderr).
Harness support
How completely predexec's design survives contact with each harness. The score is fit, not quality of the harness — it drops when predexec has to reimplement or approximate something the design wants to get natively.
| pi | opencode | Claude Code | Codex | |
|---|---|---|---|---|
| Integration | in-process extension | in-process plugin | out-of-process stdio MCP | out-of-process stdio MCP (same server as Claude Code) |
| Tool registration | native (pi.extensions) |
native (plugin array) |
MCP tool — the only route CC offers a third party | MCP tool — the only route Codex offers a third party |
read/grep/find/ls |
the host's own tool factories — exact parity | host SDK, with real caps | own implementation over node:fs (rg/fd accelerate) |
same implementation as Claude Code (mcp/tool-ops.ts is shared) |
| Steering | skill auto-loaded via pi.skills |
guarded system-prompt push, or AGENTS.md |
skill via plugin wrapper + tool description | AGENTS.md native (no plugin wrapper needed) + tool description |
| Streaming progress | yes (onUpdate) |
no | no | no |
| Host permission rules | n/a — pi has no per-command rules (project-trust only) | self-checked from permission.bash plus supported native read/grep/glob rules (with local list compatibility), last-match-wins |
self-enforced from settings.json, including mapped native read/search operations via supported Read/Grep/Glob rules (host rules don't reach a subprocess) |
self-enforced from persisted config.toml + execpolicy rules for shell/Bash only — no persisted native file-operation source and no OS sandbox backstop (MCP servers run outside it entirely, measured) |
| Published format | compiled ESM (dist/) |
compiled ESM (dist/) |
compiled ESM (dist/) |
compiled ESM (dist/) |
| Fit | 9 / 10 | 7 / 10 | 6 / 10 | 5 / 10 |
pi — 9. Everything the design wants exists natively: predexec borrows pi's real tool
implementations, so a plan's read is the read; the routing skill auto-registers; progress
streams. Nothing is approximated. The missing point is not predexec's doing — pi has no
per-command permission model to honor, so the mutation hard-stop is the only guard, and pi ships
no sandbox.
opencode — 7. Native tool registration and a real permission model predexec enforces. Points
lost to measured SDK limits that predexec can only report, not fix: grep is hard-capped at 10
matches server-side, file.read has no offset/limit and returns trimmed content, and find is
fuzzy where pi's is glob-based. An npm-installed plugin also can't auto-register a skill, so
steering falls back to a guarded system-prompt push. In addition to Bash, predexec self-checks
supported native read, grep, and glob permissions (with the repository's legacy list
compatibility shape retained).
Claude Code — 6. It works, and MCP is the only door — but out-of-process costs are real.
There are no host tool factories, so mcp/tool-ops.ts is a second implementation of
read/grep/find/ls with its own behavior (.gitignore handling, regex dialect, output format).
Your Bash(...) rules don't reach the subprocess, so predexec re-reads and enforces them
itself. No streaming progress. What it does keep is the thing that matters: the same core/
engine, the same plan tree, the same hard-stops.
Codex — 5. Literally the same mcp/server.ts and mcp/tool-ops.ts as Claude Code (started
with --host codex), so the same out-of-process costs apply: a second read/grep/find/ls
implementation, no streaming. Two things make the fit worse than Claude Code's slot. First,
Codex spawns MCP servers entirely outside its own sandbox — measured directly, not
inferred: a probe server wrote to disk with zero error while the session's own shell tool was
confined to a read-only sandbox — so there is no OS-level backstop at all, only
mcp/policy-codex.ts's config.toml/execpolicy-rules reading and predexec's own
destructive.ts heuristic (see the sandbox warning under Codex CLI below).
Its persisted policy checker is intentionally shell/Bash-only: Codex has no persisted native
file-operation rule source for predexec to mirror, so native read/search operations continue
through predexec's own read-only and containment guards.
Second, Codex's default per-call approval mode treats an unannotated tool as destructive, so
declaring readOnlyHint: true is load-bearing just to run a plan without a prompt under
default settings (per Codex's source), not merely a nicety. What's better here: AGENTS.md is native to Codex, so declarative steering doesn't need
a plugin wrapper the way Claude Code's does.
Install
pi coding agent
pi install npm:predexec
That's the whole install. pi fetches the package from npm, runs npm install --omit=dev,
and registers the predexec tool from the package's
pi.extensions manifest (plus a terse routing skill from pi.skills,
.pi/skills/predexec/SKILL.md) — runs compiled ESM from dist/. Once pi
starts, the model routes multi-step work through it on its own.
pi -e npm:predexec # try it for one run, no settings change
pi remove npm:predexec # uninstall
pi update --extensions # update installed packages
Verify:
pi list # must show npm:predexec and its install path
npx -y predexec doctor # install checks: [x] green, [!] broken, [ ] not wired
Then start pi and try the prompt under A prompt to see it work —
the tool result's details (pathTaken, stoppedReason) confirm the engine actually walked
a plan tree.
To install from the git repo HEAD instead of the published npm release:
pi install git:github.com/FuriousZen/predexec
Prerequisites: Node 22+ and the pi coding agent on PATH (npm i -g @earendil-works/pi-coding-agent), authenticated for some provider. The simplest way is an env
var — pi auto-detects provider keys from the environment (OPENCODE_API_KEY, NVIDIA_API_KEY,
OPENROUTER_API_KEY, …), so no ~/.pi editing is required.
opencode
Add predexec to your opencode.json (project root, or ~/.config/opencode/opencode.json for global):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["predexec"]
}
That's the whole install — opencode resolves the plugin from npm, loads dist/.opencode/plugins/predexec.js
in-process via Bun, and registers the predexec tool natively. No global install, no wrapper file.
Restart opencode after editing.
To update, note that "predexec@latest" does not re-resolve on its own: opencode caches the
package in a directory literally named predexec@latest and reuses it across restarts. Clear the
cache first:
rm -rf ~/.cache/opencode/packages/predexec@latest # then restart opencode
Verify (no model request needed):
npx -y predexec doctor # static install checks
npx -y predexec doctor --live # live probe: spawns opencode, confirms tool registered
opencode serve --port 4599 &
curl -s localhost:4599/experimental/tool/ids # must include "predexec"
If predexec is missing from the list, the plugin was silently skipped — opencode surfaces
plugin load failures only as internal session events, so this curl is the reliable check.
Then, in a session, try the prompt under A prompt to see it work.
The plugin injects a one-line routing rule into the system prompt as a guarded fallback.
To steer declaratively instead, copy the routing block into your project's AGENTS.md:
curl -fsSL https://raw.githubusercontent.com/FuriousZen/predexec/main/configs/opencode/AGENTS.md -o AGENTS.md
(A plugin install has no project node_modules — opencode keeps the package in its own
cache — so fetch the block from the repo, or cp configs/opencode/AGENTS.md from a clone.)
When opencode loads that natively, the plugin detects it (a quorum of routing-rule markers, not a mere mention of the name) and skips its own injection — no duplication.
For local development, opencode also auto-discovers .opencode/plugins/*.ts, so running opencode
inside a clone of this repo picks up .opencode/plugins/predexec.ts directly.
Prerequisites: the opencode CLI installed and authenticated for some provider.
predexec's payoff is largest on a request-limited free tier (OpenCode Zen free models, NVIDIA NIM, OpenRouter free).
Using the devcontainer? It lives in the parent directory of this repo, not inside it, so a plain
git cloneof predexec does not bring it along. Where it is present,post-createauto-installs predexec on every rebuild and.devcontainer/.envsupplies the provider keys.
Claude Code
Claude Code has no in-process tool-registration API for third parties — a plugin ships skills,
agents, hooks, MCP servers and LSP servers, but cannot register a tool. So predexec reaches
Claude Code as a small stdio MCP server exposing the same single predexec tool, backed by
the same core/ engine as the other two adapters.
The one-liner, no plugin required:
claude mcp add predexec -- npx -y --package=predexec predexec-mcp
Use --scope project to share it with a repo (writes .mcp.json, which each collaborator
approves once), or --scope user for every project on the machine.
Verify:
claude mcp list # predexec → ✔ Connected
npx -y predexec doctor # shows the registered scope, and flags "awaiting approval"
Then try the prompt under A prompt to see it work.
Permissions — read this one
An MCP server is a separate process, so the shell commands predexec runs inside it are not
filtered by your Claude Code Bash(...) allow/ask/deny rules. Anthropic documents this directly:
deny rules "don't apply to arbitrary subprocesses that read or write files indirectly."
predexec therefore enforces your rules itself: it reads your settings (managed →
.claude/settings.local.json → .claude/settings.json → ~/.claude/settings.json) and
hard-stops before running anything a deny or ask rule would have caught — it cannot
prompt mid-walk, so it stops instead. predexec is always at least as strict as the host, never
less. Two limits worth knowing:
--allowedTools/--disallowedToolspassed on the CLI are invisible to a subprocess and cannot be honored. Put rules you rely on in a settings file.- predexec's MCP
read/grep/find/lsoperations are self-checked against the supportedRead(...),Grep(...), andGlob(...)rules in Claude settings. Unsupported Claude permission shapes and CLI-only flags cannot be mirrored by the subprocess. For OS-level enforcement that binds every process, enable sandboxing.
MCP read/grep/find/ls paths are checked after symlink resolution and cannot leave the
session root; dependency symlinks below an exact node_modules path segment are the sole
exception.
Native operation limits are bounded before execution: read accepts at most 10,000 lines,
grep/find at most 1,000 results, ls at most 5,000 entries, grep patterns at most
8,192 characters, and grep context at most 100 lines per side. Limits and offsets are positive
integers; omitted values keep adapter defaults.
The MCP adapter revalidates canonical paths at operation boundaries, post-validates search result
paths, uses stable directory handles for local walks/listings, and opens files with O_NOFOLLOW
where the platform exposes that flag. Node does not provide a portable openat/readdirat
traversal API, so a malicious concurrent rename/replacement of a parent directory can still
race a pathname-based open or an rg/fd accelerator; this is outside the adapter's
single-process threat model.
Plugin form (optional)
The repo also carries a plugin wrapper (.claude-plugin/plugin.json) that bundles the MCP
server with a routing skill. The server config is inlined in the manifest rather than kept in
a root .mcp.json — a root .mcp.json is a live project-scope registration, so it would prompt
anyone who merely opened this repo in Claude Code. It shells out to the same npx command rather
than vendoring node_modules, so there is no dependency-bundling step.
Codex CLI
Codex has no in-process tool-registration API either, so predexec reaches it the same way it
reaches Claude Code: the identical stdio MCP server, mcp/server.ts. Only the policy reader and
stats label differ, and — because Codex clears every CODEX_* env var before spawning the
subprocess (measured; there is no equivalent of CLAUDE_PROJECT_DIR), so the server cannot
detect its host on its own — they're selected explicitly with a flag:
codex mcp add predexec -- npx -y --package=predexec predexec-mcp --host codex
codex mcp add registers globally (~/.codex/config.toml) — there's no per-project scope
flag the way Claude Code has --scope project.
Verify:
codex mcp get predexec --json # transport.command/args populated, "enabled": true
npx -y predexec doctor # shows the registered scope, flags a broken install
Timeouts. startup_timeout_sec / tool_timeout_sec defaults are version-dependent (the
docs say 10s/60s, the source at the time of writing says 30s/300s) — a long plan tree is safer
with an explicit value. Add both to ~/.codex/config.toml:
[mcp_servers.predexec]
command = "npx"
args = ["-y", "--package=predexec", "predexec-mcp", "--host", "codex"]
tool_timeout_sec = 120
Codex loads a project's AGENTS.md natively. To steer it declaratively, copy the routing
block into your project's AGENTS.md:
curl -fsSL https://raw.githubusercontent.com/FuriousZen/predexec/main/configs/codex/AGENTS.md -o AGENTS.md
Codex concatenates AGENTS.md content (repo root down to your working directory) under a 32 KiB combined cap, so keep the block as shipped rather than padding it.
Sandbox — read this one
Codex runs MCP servers OUTSIDE its sandbox. Measured directly: under a
read-onlysession sandbox, a throwaway probe MCP server still wrote a log line to disk with zero error — the child process was never inside Seatbelt (macOS) / Landlock+bwrap (Linux) at all. Every shell command a predexec plan runs bypasses Codex's sandbox entirely, regardless ofsandbox_mode/ the active permission profile, because those govern Codex's own shell tool, not an MCP subprocess. predexec's read-only invariant, thedestructive.tsheuristic, and the fail-closed execpolicy-rules adapter (reads~/.codex/config.tomlplus~/.codex/rules/*.rules/<repo>/.codex/rules/) are the only containment — there is no OS-level backstop the way Claude Code's sandboxing docs offer. Session-only CLI flags (--sandbox,-a/--ask-for-approval,--profile,--full-auto) are a config layer that never touches disk, so predexec cannot see or honor them either — parallel to Claude Code's--allowedToolsgap, except here nothing else is watching. This is not a defect predexec can fix; it is how Codex spawns MCP servers, and it means every predexec-run command deserves the same trust you'd give a command Codex's sandbox wasn't guarding at all.predexec also declares
readOnlyHint: trueon its tool so Codex's per-call approval flow (defaultauto, which otherwise treats an unannotated tool as destructive and prompts every call) runs plans without a prompt. That's an approval-UX convenience, not a sandbox, and changes nothing above.One more asymmetry worth knowing: Codex pipes MCP server stderr into its own log store, not the TUI. On the build this was verified against (0.149.1), a deliberately written stderr line did not surface in either the documented log location or its replacement — treat stderr as unrecoverable and rely on the tool's own text result, never diagnostic logging, when a plan fails.
A prompt to see it work
A read-only, structurally predictable task — predexec's sweet spot:
Detect this project's package manager and run its test script.
The model can plan one tree: probe for a lockfile / read package.json scripts, branch on
what it finds (fileExists pnpm-lock.yaml, jsonPath scripts.test exists), and run the right
test command — resolving several branch points in a single round-trip instead of one model
call per step. On pi and opencode, inspect the tool result's details (depthReached,
pathTaken, stoppedReason, edgesEvaluated/edgesMatched) to see the path the engine
walked. The Claude Code MCP adapter returns only the transcript text to the model — details
never reaches it there — so on Claude Code, check the transcript and run npx -y predexec stats for the same accounting.
Doctor & stats
predexec ships a CLI (bin/predexec.mjs, node builtins only) for install diagnostics and
request accounting:
npx -y predexec doctor # node version + pi / opencode / Claude Code / Codex wiring
npx -y predexec doctor --live # + spawns opencode and probes tool registration
npx -y predexec stats # aggregate recorded runs: ops collapsed, requests saved, edge hit-rate
doctor reports four states and exits non-zero only for [!] — a machine that simply
doesn't use a given harness is healthy, not broken:
| meaning | |
|---|---|
[x] |
wired and healthy |
[!] |
predexec IS wired here but is broken — the only state that fails |
[ ] |
harness installed, predexec not wired (actionable) |
[-] |
harness not installed |
Stats are append-only JSONL in $PREDEXEC_STATE_DIR (or $XDG_STATE_HOME/predexec, or
~/.local/state/predexec). Each adapter calls recordRun after every runPlanTree — fire-and-forget,
errors swallowed (a stats failure must never break a tool call).
Develop / contribute
Clone and use pnpm (the project's package manager):
git clone https://github.com/FuriousZen/predexec && cd predexec
corepack enable # makes pnpm available (ships with Node)
pnpm install
pnpm run build # tsc -p tsconfig.build.json -> dist/
pnpm test # vitest
pnpm run typecheck # tsc --noEmit
Load your working copy live in pi while iterating:
pi -e /path/to/predexec/.pi/extension/index.ts # or run `pi` inside the repo
(Inside the devcontainer the checkout is already mounted and the adapter loads from it, so your edits are always what's measured.)
Layout
dist/ compiled ESM JavaScript (emitted by tsconfig.build.json)
.pi/extension/index.ts pi adapter — JSON Schema + ctx wiring, delegates to core
.opencode/plugins/predexec.ts opencode adapter — zod schema + context wiring, delegates to core
mcp/ Claude Code / Codex adapter (stdio MCP), delegates to core
server.ts the MCP server: one `predexec` tool (`--host` picks the policy reader)
tool-ops.ts read/grep/find/ls over node:fs (rg/fd accelerate when present)
policy-claude.ts reads your Claude Code permission rules → policyStop
policy-codex.ts reads Codex's config.toml + execpolicy rules → policyStop, fail-closed
core/ PURE TS, zero harness imports (promotable to a standalone package)
types.ts conditions.ts runner.ts engine.ts destructive.ts coerce.ts index.ts
steering.ts shared steering text/marker (harness-facing; not in core/)
stats.ts request-accounting recorder (append-only JSONL; harness-facing)
policy.ts opencode permission reader/checker (harness-facing)
adapter-runtime.ts shared adapter execution & stats runtime
bin/predexec.mjs CLI: doctor + stats (node builtins only)
bin/predexec-mcp.mjs Claude Code / Codex MCP entrypoint (`--host codex` selects Codex)
.pi/skills/predexec/SKILL.md declarative pi routing skill (loaded via pi.skills)
skills/predexec-claude/SKILL.md Claude Code routing skill (shipped with the plugin wrapper)
.claude-plugin/plugin.json optional Claude Code plugin wrapper
configs/opencode/AGENTS.md drop-in routing block for opencode projects
configs/codex/AGENTS.md drop-in routing block for Codex projects