A previous post described how to give a coding agent a governed CLI workflow: initialize contracts, review policy, preview a call, then execute. That workflow works, and it has one weak joint.

The workflow depends on the agent choosing to follow it. A Skill is a document. If the agent skips it, nothing in the system notices.

apexe 0.10.0 closes that joint with three commands: apexe sh (and the installed apexe-sh launcher), apexe agent , and apexe hook claude. The integration now lives where the host actually decides what to run.

This post takes one host from zero to verified, then shows how to widen coverage without pretending the boundary is bigger than it is.

The gap, in one sentence
Governance is only as good as the code path the command must pass through. For a terminal agent, that path is the host's shell invocation. So you either replace the shell, pre-tool every call, or leave enforcement to the model's goodwill.

apexe now supports the first two.

  1. Install, initialize, review Use a prebuilt binary or build from source; then prepare coverage:

apexe init
apexe list --verbose
init acquires the configured rules source, matches installed commands and their variants, and writes bindings plus a starter ACL. Review ~/.apexe/acl.yaml before you go further: generated allow rules are suggestions, not your policy.

Read the init report too. native_ready tells you which routes have reviewed native grammar. The reviewed pilot is small on purpose — BSD cat, head, and ls. If your installation reports nothing else there, that is expected, not a bug.

  1. Turn on the launcher, then read the report apexe env --enable source ~/.apexe/bin/apexe-activate.sh apexe sh --check env --enable installs the PATH shims, the activation helpers, the startup block for your shell, and the stable apexe-sh entry point. apexe-sh is what a host points at when it only accepts a shell path.

apexe sh --check is the part people skip and then regret. It reports:

the real shell it will exec, and the entry point it found
the shim manifest and ACL state — a missing ACL is a warning, because shim-routed commands would then run without access control
the local environment policy and the dynamic-loader denylist (LD_PRELOAD, LD_LIBRARY_PATH, DYLD_*)
verified routes and scanned routes, listed separately
bindings that exist but were excluded from shims, each with its exclusion reason
host snippets for OpenCode, pi, Codex, and Claude Code
Two lines in that report decide your next step. If the ACL is missing, stop and load one. If a command you need appears under exclusions, the reason string tells you which reviewed fact is absent — coverage gaps are visible before you widen anything.

For machine checks, apexe sh --check --format json gives you the same data in CI.

  1. Wire one host Print first, write second:

apexe agent opencode
apexe agent --check opencode
apexe agent opencode --write
--write merges into existing JSON atomically, preserves other keys, and refuses a file that is not valid JSON. For OpenCode and pi it also refuses when apexe-sh is not installed. That refusal is the point: both hosts start every command through that path, so a missing launcher does not fall back to the system shell — the host simply fails to run anything, and you get an EACCES that names nothing useful.

The four hosts are four different integrations. Do not assume one recipe:

Host What you configure Thing to know
OpenCode shell = absolute apexe-sh path Its shell tool invokes apexe-sh -c ""
pi shellPath = apexe-sh Exports APEXE_SHELL_DEPTH=1 to nested shells
Codex Login-shell PATH activation Rejects apexe-sh as its shell; zsh activation now writes .zprofile too, so noninteractive login shells resolve shims
Claude Code PreToolUse hook, or PATH at launch Rejects apexe-sh as SHELL/CLAUDE_CODE_SHELL and falls back to /bin/zsh
Codex deserves one extra note: apexe agent codex --write has no shell file to edit, so it runs apexe env --reload --shell zsh instead. Upgrade an existing installation the same way, then restart the host.

Claude Code: hook instead of shell
apexe hook claude
Configure it as a PreToolUse hook matching Bash. A simple external command is rewritten to apexe sh -c '' through updatedInput; commands already routed through apexe are left alone, so the hook does not recurse.

Builtins that change session state (cd, export, set, alias), pipelines, redirections, and compound commands are not rewritten — a one-shot apexe sh -c cannot preserve their semantics. Those keep relying on the installed PATH shims. If the shim manifest is missing, the hook denies the call and tells you to run apexe env --enable.

apexe hook claude --persistent is the experimental answer to that limitation: it routes every Bash call through one detached governed session that preserves cwd, variables, functions, and aliases, scoped to the host session id and project directory, with an idle timeout and an explicit stop. Try it on a project you can afford to re-run.

  1. Verify from inside the agent Ask the agent to run:

command -v ls
apexe exec -- ls -la
command -v ls should print the shim path under ~/.apexe/bin/, and ~/.apexe/audit.jsonl should gain an execution record. If command -v still points at /bin/ls, the host is not using your launcher — check apexe agent --check before touching anything else.

This is also the honest test of "did I enforce anything": if the agent can still reach /bin/ls with PATH intact, you widened convenience, not a boundary.

  1. Widen coverage on purpose Your shims cover reviewed routes. Everything else has a binding but no reviewed grammar, which is most of a real machine.

0.10.0 lets you admit those bindings explicitly:

apexe env --enable --scanned readonly
apexe sh --check
A scanned route parses and validates the caller's argv against the scanned schema, then executes the original argv. It never rebuilds or reorders your command line. Every such execution is audited with tier: scanned.

Three properties to keep straight:

readonly admits only readonly, non-destructive, closed-world modules. all admits any current binding — use it after reading the bindings, not before.
Schema inference is not a reviewed contract. Path-guard coverage for arguments the scanner did not type can be incomplete.
--scanned lowers the grammar bar for declared routes; it cannot widen the set. A route an overlay left out stays out.
Trust is now stamped per route (metadata.grammar), which changes what a partial overlay buys you. A mode: merge overlay that fully covers one route makes that route reviewed while its siblings stay closed. Before this release, a git-shaped tool needed a tool-wide authoritative overlay, which erased every subcommand — there was simply no usable path for git.

Audit what your overlays left behind:

apexe list --diagnostics
You get the closed routes per overlay, and — when a policy is loaded — ACL grants that match no loaded module. The second list is usually a scan artefact, and it is the only place those dead permissions are visible without reading acl.yaml by hand.

  1. Know exactly what you bought Two trust decisions are baked into 0.10.0 and both are worth saying out loud.

Local execution now inherits your environment. apexe sh, PATH shims, and native apexe exec pass the caller's environment to the child so GH_TOKEN, AWS_PROFILE, KUBECONFIG, and SSH_AUTH_SOCK keep working. Only dynamic-loader variables (LD_PRELOAD, LD_LIBRARY_PATH, DYLD_*) are stripped. Captured execution — MCP, A2A, module JSON — still clears the environment and passes a small allowlist. So: a locally governed tool runs with your credentials. That is a deliberate trust decision, and it is not isolation.

Approval is now answerable. A route marked requires_approval prompts on the controlling terminal and reads y/yes with a bounded wait. A non-interactive process is refused with a remedy instead of hanging. MCP keeps its client-based elicitation path; A2A approval stores remain a library integration.

And the boundary itself:

apexe sh --locked --allow-path /usr/bin -c 'ls'
--locked restricts PATH to the shim directory plus explicit --allow-path entries. It reduces accidental discovery of ungoverned binaries. It is not a sandbox: absolute paths, builtins, functions, sudo, and interpreter-internal execution still work. APEXE_SHELL_LOCKED and APEXE_SHELL_ALLOW_PATH apply the same setting to apexe-sh, which forwards every argument to the real shell.

apexe-sh changes command lookup order. Commands with a reviewed shim are governed; everything else is not. Pair it with the account, container, or VM boundary your workload needs, and read the threat model.

  1. Three fixes you will feel on day one git -v and git status -v no longer collide. The argv parser kept one flat flag-spelling map per module, so one spelling reused on both sides of a subcommand made every route of that tool fail before a single argument was read. Tool-level spellings now live in their own namespace and resolve by the side of the subcommand the token appears on. Git-shaped tools became usable.

x-apexe-exec is enforced on scanned routes. A scanned route replays the caller's argv, so a value like git -c core.pager= never reached the exec-parameter rejection and ran as a command. The check now runs on the parsed input both tiers produce — which matters precisely because the scanned tier exists.

A refusal names the route you asked for. Routes are validated when selected, not before matching, so a sibling without reviewed grammar no longer fails your command, and the message says Route '' instead of blaming whichever module sorted first.

There is one recovery path worth knowing if you deploy on shared machines: apexe env --enable --replace-unowned regenerates the reserved entry point and activation helpers when the ownership record is gone. Without it, a lost ~/.apexe-shims.json left the installation unrecoverable except by deleting files by hand. --reload and --disable also read that record first, so --disable now warns when a host still points at the entry point it just removed.

Troubleshooting
Symptom Likely cause
Host cannot run any command, EACCES, nothing mentions apexe apexe-sh is not installed; run apexe env --enable, then apexe agent --check
command -v ls shows /bin/ls Host is not using the launcher, or a login shell rebuilt PATH; use the managed startup block
Codex runs ungoverned commands zsh login shells read .zprofile; run apexe env --reload --shell zsh and restart
Claude Code ignores the shell It rejects apexe-sh as SHELL; use the hook or start it with the shim directory on PATH
A command you need is not shimmed apexe sh --check lists it with the exclusion reason; cover that route in an overlay or widen with --scanned readonly
A 2.0 overlay does not validate Expected — commands[] and global_flags are a preview this build accepts; the published schema still pins 1.0/1.1
Close
The interesting shift is not that apexe gained three subcommands. It is that "the agent follows the policy" stopped being an assumption. Configure one host, run apexe sh --check, then try to make an ungoverned command slip through — that exercise tells you more about your setup than any feature list.

apexe init
apexe env --enable
apexe sh --check
apexe agent --check
apexe agent --write
Repository: github.com/aiperceivable/apexe · Apache-2.0 · Shell integration guide · Threat model

Which host are you wiring up first — and which command did you decide to leave ungoverned on purpose?