predexec

extensionmaintained

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.

by — · v0.4.1 · published 3w ago

$ pi install npm:predexec
downloads/mo
598
stars
1
last push
4w ago
open issues
0

Signals

license: MITtestspi manifest: missinginstall size: —deps: 0peer deps: 0

Download trend

No downloads in the last 12 weeks.

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.

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:

stopmeaning
leafno edges — success path complete (the only non-fallback stop)
noEdgeMatchno edge matched — benign miss, agent resumes normally
maxDepthdepth cap hit
mutationStopnext node writes/installs/deletes — hard stop before any mutation
errorinvalid plan (returned gracefully, never thrown)
abortedabort 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.

piopencodeClaude CodeCodexAntigravity
Integrationin-process extensionin-process plugin (v1 {id,server} and v2 {id,setup})out-of-process stdio MCPout-of-process stdio MCP (same server as Claude Code)out-of-process stdio MCP (same server as Claude Code)
Tool registrationnative (pi.extensions)native (plugin array)MCP tool — the only route CC offers a third partyMCP tool — the only route Codex offers a third partyMCP tool — the only route agy offers a third party
read/grep/find/lsthe host's own tool factories — exact parityv1: host SDK, with real caps. v2: no file API at all, so it shares Claude Code's mcp/tool-ops.tsown 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)
Steeringskill auto-loaded via pi.skillsguarded system-prompt push, AGENTS.md, or (v1) an auto-registered packaged skillskill via plugin wrapper + tool descriptionAGENTS.md native (no plugin wrapper needed) + tool description, or a plugin-bundled skillplugin-bundled skill, install-skill, or AGENTS.md fallback + tool description
Streaming progressyes (onUpdate)nononono
Host permission rulesn/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 cannotself-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 formatcompiled ESM (dist/)compiled ESM (dist/)compiled ESM (dist/)compiled ESM (dist/)compiled ESM (dist/)
Fit9 / 107 / 106 / 105 / 105 / 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):

harnessplugin pathmanual pathskill step
pipi 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"] stepnone 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-mcpnpx -y predexec install-skill claude (manual path only)
Codexcodex 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 codexnpx -y predexec install-skill codex (manual path only); or the no-MCP AGENTS.md fallback
Antigravityno 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.

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 clone of predexec does not bring it along. Where it is present, post-create auto-installs predexec on every rebuild and .devcontainer/.env supplies 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.json already inlines the same MCP server; also running claude mcp add registers a second predexec server 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 / --disallowedTools passed 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/ls operations are self-checked against the supported Read(...), Grep(...), and Glob(...) 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 into the subprocess (env_vars: ["CODEX_HOME"]) — a bare codex mcp add cannot do 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, add it by hand (npx -y predexec doctor flags a registration that's missing this):

[mcp_servers.predexec]
env_vars = ["CODEX_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"]
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-only session 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 of sandbox_mode / the active permission profile, because those govern Codex's own shell tool, not an MCP subprocess. predexec's read-only invariant, the destructive.ts heuristic, and the fail-closed execpolicy-rules adapter (reads ~/.codex/config.toml merged over /etc/codex/config.toml for 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 --allowedTools gap, 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: true on its tool so Codex's per-call approval flow (default auto, 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's mcp_config.json schema 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 antigravity refuses every plan under the plugin form until it's told where the workspace is: export PREDEXEC_ROOT in the shell that launches agy (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 of mcp_config.json after 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:

  1. --root <dir> in the server args, or PREDEXEC_ROOT=<dir> in the server env.
  2. 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.
  3. Otherwise, the nearest ancestor of the launch dir that contains .git or .agents and lies strictly below $HOME. The walk never selects $HOME or anything above it, so a stray ~/.agents cannot widen the root to your whole home directory.
  4. 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 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 / 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 [!].

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) — used by both
                                    destructive.ts and every host policy adapter below
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
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/probes/                    throwaway measurement scripts behind docs/research/* findings
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)
.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)