Outside-In CLI-to-Agent Bridge — automatically wraps existing CLI tools into governed apcore modules, served via MCP.
apexe scans any CLI tool on your system (e.g., git, curl, ls, jq), deterministically extracts its command structure, flags, and arguments — then exposes them as governed MCP tools that AI agents can invoke safely.
No LLM required for scanning. No changes to the CLI tools. Zero-config governance.
- Scan — Three-tier deterministic engine (--help → man pages → shell completions) with 6 built-in parsers (Man, BSD Usage, GNU, Click, Cobra, Clap)
- Schema — Generates JSON Schema with type mapping, format hints (
path,uri), defaults, enums, and required fields - Serve — MCP server via apcore-mcp (stdio / streamable-http / SSE) with an Explorer UI, plus an A2A agent server via apcore-a2a. Transport authentication is a first-class CLI surface (
--auth token|jwt|none) required by default on the HTTP-family transports; a non-loopback bind with no credential refuses to start unless explicitly acknowledged - Govern — Behavioral annotations (readonly/destructive/idempotent), flag boosting (
--force→ requires_approval), fail-closed default-deny ACL, a JSONL audit trail of executions, refusals and ACL allow/deny decisions (no input values, hashed or otherwise; log0600),Module::preview()dry-run for destructive commands - Isolate — every wrapped subprocess runs with the environment scrubbed to a base allowlist (secrets don't leak to tools), no shell (argv goes straight to
execve, so shell metacharacters are inert data — the actual guard rejects a value that would be parsed as an option), output capped at 64 MiB, and a hard timeout that actually kills the process (kill_on_drop); circuit breaker + retry middleware on by default, optional/metrics+/usage - AI Guidance — Every error includes
ai_guidanceto help agents self-correct; non-zero exit codes return stderr context
| Crate | Role |
|---|---|
| apcore 0.27 | Module trait, Registry, ACL, ModuleError, Context |
| apcore-toolkit 0.10 | ScannedModule, YAMLWriter, DisplayResolver |
| apcore-mcp 0.18.1 | MCP server with middleware, auth, Explorer UI |
| apcore-a2a 0.5 | A2A agent server sharing the same governed Executor |
| apcore-cli 0.10 | --man page generation |
No Rust toolchain required. Download the archive for your platform from the
Releases page, verify the
checksum, and put the apexe binary on your PATH:
tar -xzf apexe-<target>.tar.gz
shasum -a 256 -c apexe-<target>.tar.gz.sha256
sudo mv apexe-<target>/apexe /usr/local/bin/
apexe --versionAvailable targets: x86_64-apple-darwin, aarch64-apple-darwin,
x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu.
Requires Rust 1.75+ and Cargo.
git clone https://github.com/aiperceivable/apexe.git
cd apexe
cargo install --path .
apexe --versionapexe scans Unix CLIs. What you get depends on the host:
| Host | Scanning | Curated overlays |
|---|---|---|
| macOS | Full — all four tiers | 21 commands (bsd, apple) |
| Linux | Full — install man-db for tier 2 |
21 commands (gnu) |
| FreeBSD | Full | bsd overlays apply |
| OpenBSD / NetBSD / DragonFly | Full | none — falls back to scanning |
| Other Unix (Solaris, illumos, AIX) | Tiers 1–2 | none |
| Windows | Not supported | none |
The GNU overlays carry no platform condition — they are selected by probing the
binary — so they apply anywhere GNU tools are installed, including WSL and a
macOS box with Homebrew coreutils. The BSD overlays declare macos and
freebsd, which is why the other BSDs fall back to scanning: their tools are
separately evolved code, not the same source, so claiming otherwise would be a
guess.
On Windows apexe runs but produces little. Tier 2 shells out to man and
tier 3 to zsh; both are absent, so both return nothing rather than failing
loudly. Tier 1 still runs --help, but the parsers target Unix conventions
while Windows help is /? output with /X switches. Expect near-empty results,
not an error message. Native Windows support means a PowerShell reflection path
and a /? parser, and is not implemented.
# Scan git — extracts commands, flags, types, annotations
apexe scan git
# See what was generated
apexe list
# Start MCP server (Claude Desktop / Cursor)
apexe serve
# Or HTTP with browser-based tool explorer
apexe serve --transport http --port 8000 --explorerapexe serve --show-config claude-desktop
# Copy output to ~/Library/Application Support/Claude/claude_desktop_config.json
# Restart Claude Desktop — git commands appear as MCP toolsThe snippet carries the rest of the command line (--modules-dir, --prefix,
--tags, --acl, --name, the governance toggles), so pass the flags you
intend to serve with. --auth-token / --jwt-secret are deliberately never
included.
apexe serve --show-config cursor
# Add to Cursor's MCP settingsScan CLI tools and generate binding files + ACL rules.
apexe scan ls jq curl # Scan multiple tools
apexe scan git --depth 3 # 3 levels of subcommands (default: 2, max: 5)
apexe scan git --no-cache # Force re-scan
apexe scan git --format json # Output as JSON (also: yaml, table)
apexe scan git --output-dir ./out # Custom output directory
apexe scan git --skills-dir ./out # Also write .claude/skills/<id>/SKILL.md per module
apexe scan ls --overlay ./ls.json # Apply a curated overlay instead of trusting the scanA command name does not identify a program: macOS /bin/ls is BSD, Linux
/usr/bin/ls is GNU coreutils, Alpine is BusyBox, and Homebrew coreutils puts
GNU ls on a macOS box. Every scan probes the binary (<binary> --version) and
records the result as variant, then looks for a curated overlay matching
(command, variant, version_range).
The vocabulary is bsd, gnu, apple, busybox, unknown. gnu is the
whole GNU family, not just coreutils — diffutils, tar, sed and bash are
separate projects, and the specific package is recorded in provenance.package
and pinned, where it matters, in match.probe.output_contains. apple covers
Apple's own ports, whose banner names Apple rather than BSD (macOS sort
reports 2.3-Apple (197), git reports (Apple Git-155)).
match.platform, by contrast, is an open string taken verbatim from Rust's
std::env::consts::OS — macos, linux, freebsd, openbsd, netbsd,
dragonfly, solaris and anything else a host reports. Operating systems are
not an enumerable set, so naming a new one needs no apexe change. There is
deliberately no Linux distribution dimension: GNU coreutils ls has the same
83 flags on Debian, Ubuntu and Fedora across three different releases, while
BusyBox ls has 21 — divergence tracks the implementation, which variant
already captures.
The classification order is load bearing: BSD is tested before GNU, because
macOS grep reports grep (BSD grep, GNU compatible) 2.6.0-FreeBSD and is a
BSD tool advertising GNU compatibility rather than a GNU tool. Apple is tested
only after <arch>-apple-<os> target triples are stripped, because
curl 8.7.1 (x86_64-apple-darwin25.0) is not an Apple port. See
Authoring Tool Overlays.
An overlay is a reviewed description of one variant of one command. In
authoritative mode it replaces the scan's flags, positional args and
subcommands entirely; in merge mode it overrides matching flags and adds
missing ones. Overlays are the only source that can state conflicts_with,
because no help or man page format expresses mutual exclusion machine-readably.
Overlays are loaded from, in increasing precedence: the built-ins compiled into
apexe (overlays/), ~/.apexe/overlays/*.{json,yaml}, and --overlay <PATH>.
The format is defined by schemas/tool-overlay.schema.json.
Every emitted flag carries sources and a derived confidence:
verified (overlay) > high (completion spec) > medium (two independent
heuristic sources agree) > low (a single unconfirmed source).
An overlay flag may also declare long_running: true — "this option may make
the command never terminate on its own", which is why tail -f hangs an agent
until the harness timeout. It reaches the emitted contract as the JSON Schema
extension keyword x-apexe-long-running, so an executor can bound the timeout
or refuse instead of blocking. The claim is possibility, not certainty: BSD
tail -f returns immediately when its input is a pipe.
Writing one. An overlay claiming confidence: verified must carry a
provenance block recording how it was checked — which build was consulted,
which document was read (man-page / help / vendor-docs), and when. The
schema rejects a verified overlay without it, because a verified +
authoritative overlay replaces the entire scan result with an assertion
nobody can re-check.
Read the flag list off the tool itself, never from memory:
man -P cat ls | col -b # BSD / macOS (strips overstrike)
docker run --rm debian:stable-slim ls --help # GNU coreutils
docker run --rm alpine ls --help # BusyBoxThere is deliberately no apexe overlay verify command. A tool can compare
flag names, but not descriptions, conflicts_with, types or enum values — and
those are the reason overlays exist. A green check covering only the mechanical
half would read as "this overlay is correct".
See Authoring Tool Overlays for the full procedure,
including how to cross-check a draft against a reference installation and the
traps already hit in practice (of the 38 flags ls shares between its BSD and
GNU variants, zero have identical descriptions — never copy one across).
Start MCP server for scanned tools.
apexe serve # stdio (default)
apexe serve --transport http --port 8000 # HTTP
apexe serve --transport http --port 8000 --explorer # HTTP + browser UI
apexe serve --transport sse --port 8000 # Server-Sent Events (deprecated upstream; prefer http)
apexe serve --show-config claude-desktop # Print integration config (carries the other flags; never a credential)
apexe serve --name my-tools # Custom server name
apexe serve --transport http --metrics # + /metrics (Prometheus) and /usage
apexe serve --no-circuit-breaker --no-retry # Disable resilience middleware
apexe serve --transport http --auth-token "$TOKEN" # Pin the bearer token (else one is generated)
apexe serve --prefix cli.git # Serve only git tools (not listed AND not callable)
apexe serve --no-log-arguments # Drop argument payloads from every log event, errors includedHTTP/SSE transports require a bearer token by default; a token is generated and written to stderr at startup on a loopback bind (not through the log pipeline, so
--log-level warnstill shows it).--auth noneon a non-loopback bind refuses to start without--allow-unauthenticated-bind. stdio is unaffected. Seedocs/user-manual.md.
Start an A2A agent server for scanned tools. Shares governance (ACL, logging, audit) with apexe serve via the same Executor.
apexe a2a # http://127.0.0.1:8000 (default)
apexe a2a --url http://127.0.0.1:9000 --explorer # Custom port + browser UI
apexe a2a --acl ~/.apexe/acl.yaml # Governed by an ACL policy
apexe a2a --cors-origin https://example.com # Allow a browser origin
apexe a2ahas no transport authentication. The--auth*flags areapexe serveonly, so there is no credential to opt into — bind A2A to loopback, or put it behind a reverse proxy that authenticates. A non-loopback--urlrefuses to start without--allow-unauthenticated-bind.
A2A has no interactive elicitation transport, so there is no
--enable-approvalflag onapexe a2a(it's available onapexe serve, and on A2A only via the libraryApprovalStoreAPI). Seedocs/user-manual.md.
List registered modules.
apexe list # Table format
apexe list --format json # JSON formatShow or initialize configuration.
apexe config --show # Print resolved config (YAML)
apexe config --init # Create ~/.apexe/config.yamlCLI Tool Binary
|
v
+--------------------+
| Scanner Engine | <-- Tier 1: --help (GNU/Click/Cobra/Clap)
| | <-- Tier 2: man pages (DESCRIPTION + OPTIONS)
| | <-- Tier 3: shell completions (subcommand discovery)
+---------+----------+
| ScannedCLITool
v
+--------------------+
| Adapter Layer | <-- module IDs, JSON Schema, annotations, display metadata
+---------+----------+
| ScannedModule (apcore-toolkit)
|
+-----+-----+
| |
v v
+--------+ +------------------+
| Output | | MCP Server |
| .yaml | | apcore-mcp |
| ACL | | stdio/http/sse |
| Audit | | middleware+auth |
+--------+ +------------------+
| Signal | Inference |
|---|---|
Command list, show, status, get |
readonly: true, cacheable: true |
Command delete, rm, kill, destroy |
destructive: true, requires_approval: true |
Flag --force, -f, --hard |
Escalates to requires_approval: true |
Flag --dry-run, --check, --simulate |
idempotent: true |
| CLI Type | JSON Schema |
|---|---|
--message "hello" |
"type": "string" |
--count 5 |
"type": "integer" |
--config /path |
"type": "string", "format": "path" |
--url https://... |
"type": "string", "format": "uri" |
--format json|yaml |
"type": "string", "enum": ["json","yaml"] |
--include a --include b |
"type": "array", "items": {"type":"string"} |
Resolved in 4 tiers (highest wins): CLI flags > env vars > config file > defaults
apexe config --init # Creates ~/.apexe/config.yaml| Env Variable | Default | Description |
|---|---|---|
APEXE_MODULES_DIR |
~/.apexe/modules |
Binding file storage |
APEXE_CACHE_DIR |
~/.apexe/cache |
Scan cache |
APEXE_LOG_LEVEL |
info |
Log level |
APEXE_TIMEOUT |
30 |
CLI subprocess timeout (seconds) |
APEXE_SCAN_DEPTH |
2 |
Subcommand recursion depth |
| Path | Purpose |
|---|---|
~/.apexe/config.yaml |
Configuration |
~/.apexe/modules/*.binding.yaml |
Generated tool bindings |
~/.apexe/cache/ |
Scan result cache |
~/.apexe/acl.yaml |
Access control rules |
~/.apexe/audit.jsonl |
Audit trail |
See examples/README.md for full details.
| Example | Description | Run |
|---|---|---|
| basic | Shell script: scan → list → serve | ./examples/basic/run.sh |
| programmatic | Rust library: scan → convert → export OpenAI tools → build MCP server | cargo run --example programmatic |
| acl_demo | Rust library: role-based ACL rules on CliModule calls via Executor |
cargo run --example acl_demo |
cargo build # Build
cargo test --all-features # Run tests (~338)
cargo test -- --include-ignored # Include integration tests
cargo clippy --all-targets --all-features -- -D warnings # Lint
cargo fmt --all -- --check # Format check
cargo run --example programmatic # Run exampleImplement the CliParser trait in src/scanner/protocol.rs:
pub trait CliParser: Send + Sync {
fn name(&self) -> &str;
fn can_parse(&self, help_text: &str) -> bool;
fn parse(&self, help_text: &str, tool_name: &str) -> anyhow::Result<ParsedHelp>;
fn priority(&self) -> u32; // lower = tried first
}RUST_LOG=debug apexe scan git
apexe --log-level trace scan gitapdev-rs release handles tagging, the GitHub Release, and publishing to
crates.io; pushing the rust/vX.Y.Z tag it creates automatically triggers
.github/workflows/release.yml, which builds
the prebuilt binaries described in Installation and attaches
them to that release. No separate step is needed for a normal release.
To backfill binaries onto a release that's missing them (e.g. one published before this workflow existed), dispatch it manually against the existing tag:
gh workflow run release.yml -f tag=rust/vX.Y.Z| Document | Description |
|---|---|
| Quick Start | Get running in 30 seconds |
| User Manual | Full reference — commands, config, scanning, schema generation, annotations, governance, MCP server, AI integration, error handling |
| Authoring Tool Overlays | How to write and verify a curated overlay — required reading before adding one |
| Examples | Shell script walkthrough + Rust library API usage |
| Changelog | Release history and migration notes |
| Document | Description |
|---|---|
| Technical Design | v0.1.0 architecture with apcore ecosystem integration |
| Feature Manifest | Module map, crate dependencies, project status |
| Feature Specs | Detailed specifications for features F1-F7 |
| Spec | Description |
|---|---|
| F1: Scanner Adapter | ScannedCLITool → ScannedModule conversion |
| F2: Module Executor | apcore Module trait for CLI subprocess execution |
| F3: Binding Output | apcore-toolkit YAMLWriter integration |
| F4: MCP Server | apcore-mcp server builder |
| F5: Governance | apcore ACL wrapper + apexe's own audit trail |
| F6: Error Migration | ApexeError → ModuleError conversion |
| F7: Config Integration | apcore Config integration |
Apache-2.0