A durable command runner for AI agents: start a command now, observe it briefly, then retrieve or control the job later.
Agent harnesses often need to run tests, builds, and deployments whose duration and output size are unknown. A synchronous subprocess can block the agent, while a detached shell command loses structured status and log discovery. agent-exec keeps the process running under a stable job_id and returns machine-readable JSON for later status, tail, wait, kill, or restart calls.
Prebuilt binaries are available for Linux x86_64 and macOS Apple Silicon. This example downloads the latest release, verifies its SHA-256 checksum, and installs it in ~/.local/bin:
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) TARGET=x86_64-unknown-linux-gnu ;;
Darwin-arm64) TARGET=aarch64-apple-darwin ;;
*) echo "No release binary for this platform" >&2; exit 1 ;;
esac
VERSION=$(curl -fsSL https://api.github.com/repos/tumf/agent-exec/releases/latest | sed -n 's/.*"tag_name": "v\([^"]*\)".*/\1/p')
ARCHIVE="agent-exec-v${VERSION}-${TARGET}.tar.gz"
curl -fLO "https://github.com/tumf/agent-exec/releases/download/v${VERSION}/${ARCHIVE}"
curl -fLO "https://github.com/tumf/agent-exec/releases/download/v${VERSION}/${ARCHIVE}.sha256"
shasum -a 256 -c "${ARCHIVE}.sha256"
mkdir -p ~/.local/bin
tar -xzf "$ARCHIVE"
install agent-exec ~/.local/bin/agent-exec
~/.local/bin/agent-exec --versionStart a long-running command without blocking the caller, save its job_id, then retrieve its status and final output:
AGENT_EXEC=~/.local/bin/agent-exec
JOB=$($AGENT_EXEC run --no-wait -- sh -c 'sleep 2; echo done' | sed -n 's/.*"job_id":"\([^"]*\)".*/\1/p')
$AGENT_EXEC status "$JOB"
$AGENT_EXEC wait "$JOB"
# Use tail later to reread bounded logs or inspect their paths.
$AGENT_EXEC tail "$JOB"run normally observes a job for up to 10 seconds, which catches many startup failures without blocking indefinitely. Use --no-wait when the caller must return immediately. wait reports the state, exit code when terminal, and bounded output available at the observation deadline. Use tail later to reread bounded logs. Complete logs remain available at the paths in each response.
| Capability | Plain subprocess | nohup |
agent-exec |
|---|---|---|---|
| Return without stopping the command | No | Yes | Yes |
| Stable job identifier | No | No | Yes |
| Structured status and exit result | Limited | No | Yes |
| Discoverable stdout/stderr logs | Caller-managed | Caller-managed | Yes |
| Later wait, tail, kill, and restart | Caller-managed | Caller-managed | Yes |
Pass ordinary commands as argv after --. Use an explicit shell only when the workload needs pipelines, redirects, expansion, or compound statements.
Response-producing CLI commands write exactly one JSON object to stdout by default. --yaml changes those responses to YAML. Diagnostic logs go to stderr and are controlled by RUST_LOG, -v, and -vv.
This contract applies to commands such as run, status, tail, list, gc, and install-skills. It does not apply to generated shell completions, the MCP stdio protocol, the HTTP server, MCP startup configuration errors, or Clap help and version output.
run, start, restart, and tail include a compressed view by default while preserving the raw excerpt and byte metadata as the canonical output.
- Default mode:
route - CLI selection:
--compress <MODE>or--rtk <MODE> - Modes:
off,route,errors,tests,logs,git,json, andsummary - Compatibility mode:
--compress offomits thecompressionfield - Configuration:
[compression].default = "off"or another supported mode
Precedence is --compress or --rtk, [compression].default, then the built-in route default. If compression would not make either nonempty stream smaller, the expansion guard sets applied to false, returns empty compressed streams, and records "expansion-guard" in strategy. The raw stdout, stderr, ranges, totals, encoding, and log paths remain unchanged.
If you are integrating through MCP, see MCP Server for the same lifecycle as tools. To see the observation deadline and reconnect behavior end to end, run the one-minute demo.
GitHub Actions builds the Linux x86_64 archive for each v* release. Download its matching checksum, then verify and install:
ARCHIVE=agent-exec-v<VERSION>-x86_64-unknown-linux-gnu.tar.gz
shasum -a 256 -c "$ARCHIVE.sha256"
mkdir -p ~/.local/bin
tar -xzf "$ARCHIVE"
mv agent-exec ~/.local/bin/
agent-exec --version
agent-exec run -- echo "release-smoke"macOS artifacts are built locally on the trusted mini host after GitHub Actions creates the release. From a checkout at the release tag:
scripts/release-macos.sh --tag v<VERSION>
scripts/release-macos.sh --tag v<VERSION> --uploadThe first command builds, smoke-tests, packages, and checksums the native architecture archive without network mutation. The second uploads it to the existing GitHub Release. Install the resulting macOS archive with the checksum, extraction, version, and managed-command steps above.
Windows release binaries are not provided. ARM, 32-bit, musl Linux, Windows, and other unsupported targets can use crates.io or a source build.
A Rust toolchain can install the published package:
cargo install agent-exec --locked
agent-exec --version
agent-exec run -- echo "release-smoke"For development or unsupported targets, build from a repository checkout:
cargo install --path . --lockedagent-exec generates dynamic completion scripts for Bash, Zsh, Fish, and PowerShell.
All job ID candidates are limited to jobs whose persisted current working directory matches the caller's current working directory. Candidates come from the resolved jobs root. Entries with missing or mismatched current working directory metadata are excluded.
State filters depend on the command:
| Command | Candidate states |
|---|---|
status, tail, restart, tag set, notify set |
All known job IDs; unreadable states may still appear |
start |
created |
wait |
created, running |
kill |
running |
delete |
exited, killed, failed |
Completion is advisory. Command implementations still validate the selected job and may support behavior not offered by completion.
agent-exec completions bash >> ~/.bash_completion
source ~/.bash_completionmkdir -p ~/.zsh/completions
agent-exec completions zsh > ~/.zsh/completions/_agent-exec
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinitmkdir -p ~/.config/fish/completions
agent-exec completions fish > ~/.config/fish/completions/agent-exec.fishagent-exec completions powershell | Out-String | Invoke-ExpressionAfter registration, job ID completion is available at command arguments such as:
agent-exec tail <TAB>
agent-exec kill <TAB>The default run behavior observes the job for up to 10 seconds and returns inline output:
agent-exec run -- echo "hello world"The following is an illustrative complete response for a successful terminal job. Paths, timestamps, and timing values vary.
{
"schema_version": "0.1",
"ok": true,
"type": "run",
"job_id": "7f3a9c1e4b2d8a605e7c9f0134ab6d82",
"state": "exited",
"tags": [],
"stdout_log_path": "/home/user/.local/share/agent-exec/jobs/7f3a9c1e4b2d8a605e7c9f0134ab6d82/stdout.log",
"stderr_log_path": "/home/user/.local/share/agent-exec/jobs/7f3a9c1e4b2d8a605e7c9f0134ab6d82/stderr.log",
"elapsed_ms": 8,
"waited_ms": 2,
"stdout": "hello world\n",
"stderr": "",
"stdout_range": [0, 12],
"stderr_range": [0, 0],
"stdout_total_bytes": 12,
"stderr_total_bytes": 0,
"encoding": "utf-8-lossy",
"exit_code": 0,
"finished_at": "2026-07-19T12:00:00Z",
"duration_ms": 1,
"compression": {
"mode": "route",
"applied": false,
"detected_kind": "summary",
"stdout": "",
"stderr": "",
"stdout_original_bytes": 12,
"stderr_original_bytes": 0,
"stdout_compressed_bytes": 0,
"stderr_compressed_bytes": 0,
"omitted": false,
"strategy": ["expansion-guard"]
}
}Generated job IDs are 32-character lowercase hexadecimal strings. Commands that accept a job ID also accept an unambiguous prefix and return the canonical full job ID.
Use --no-wait when the launch response must return immediately, then inspect the job separately:
JOB=$(agent-exec run --no-wait -- sleep 30 | jq -r .job_id)
agent-exec status "$JOB"
agent-exec tail "$JOB"
agent-exec wait "$JOB"Without --no-wait, run observes for up to 10 seconds before returning.
The following command sends SIGTERM after 5 seconds and SIGKILL 2 seconds later if necessary:
agent-exec run \
--timeout 5 \
--kill-after 2 \
-- sleep 60Pass ordinary commands as argv after --:
agent-exec run -- sleep 8
agent-exec run -- cargo test --all
agent-exec run -- npm run buildUse an explicit shell only when the workload requires shell syntax such as pipelines, redirects, expansion, or compound statements:
agent-exec run -- sh -lc 'sleep 8; echo done'create persists a job definition without starting a process. start launches it later.
JOB=$(agent-exec create -- echo "deferred hello" | jq -r .job_id)
agent-exec start "$JOB"createwrites the execution definition tometa.json, initializesstate.jsonwithstateset tocreated, and returnstypeset tocreate.startreads the persisted definition, launches the supervisor, and observes for up to 10 seconds by default.restartreuses the job ID and definition. It terminates a running process tree before launching the replacement run.runcombines definition and launch in one command.
Values passed through create --env KEY=VALUE are durable configuration. The real values are stored in meta.json for the later start, while the display-oriented env_vars metadata applies --mask. Use --env-file FILE when values should be read from a file at start time instead of being stored directly in the job definition.
--mask KEY only replaces the named --env value in display-oriented environment metadata and response fields. It does not redact child stdout, child stderr, persisted logs, notification payload content, or values loaded from --env-file. A child process that prints a secret will expose it in its output. Do not treat --mask as a general secret-filtering mechanism.
| State | Meaning |
|---|---|
created |
Definition persisted; no process started |
running |
Supervisor and child process active |
exited |
Process exited normally |
killed |
Process terminated by a signal |
failed |
Supervisor-level failure |
kill rejects created jobs because there is no process to signal. wait continues through created and running until a terminal state or its client-side deadline. list --state created selects jobs that have not started.
| Flag | Default | Description |
|---|---|---|
--root <PATH> |
Platform data directory | Override the jobs root. Precedence is --root, AGENT_EXEC_ROOT, $XDG_DATA_HOME/agent-exec/jobs, then the platform default. |
--yaml |
false |
Emit YAML instead of JSON for response-producing CLI commands. |
-v, -vv |
Warnings | Increase diagnostic verbosity on stderr. |
Place global options before the subcommand:
agent-exec --root /tmp/jobs run -- echo hello
agent-exec --root /tmp/jobs status <JOB_ID>
agent-exec --root /tmp/jobs list
agent-exec --root /tmp/jobs gc --dry-runFor backward compatibility, job-store commands also accept --root after the subcommand where defined:
agent-exec run --root /tmp/jobs -- echo hello
agent-exec status --root /tmp/jobs <JOB_ID>Use agent-exec --help and agent-exec <COMMAND> --help for the complete current CLI surface.
agent-exec create [OPTIONS] -- <COMMAND> [ARGS...]create persists execution-definition options for the command, effective working directory (--cwd, or the caller's current working directory), environment and inheritance, input, runtime limits, progress updates, tags, completion and output-match notifications, and shell configuration. Materialized input is limited by --stdin-max-bytes. It does not accept launch observation, compression, or automatic GC options. Use agent-exec create --help for the complete option list.
--stdin VALUE and --stdin-file PATH are mutually exclusive. Their contents are copied to <job-directory>/stdin.bin; later start reuses the persisted file reference.
The response includes job_id, state, stdout_log_path, and stderr_log_path.
agent-exec start [OPTIONS] <JOB_ID>Only a job in created state can start. By default, start observes for up to 10 seconds and returns the same inline stream fields as run. Observation controls include --wait [true|false], --until, --forever, --no-wait, --max-bytes, and compression selection. Automatic GC controls are also available.
agent-exec restart [OPTIONS] <JOB_ID>restart preserves the job ID and persisted definition. If the job is running, it sends the signal selected by --signal and confirms termination before relaunching. It clears prior-run stream logs, full.log, and stale completion_event.json so subsequent observation reflects the replacement run.
restart supports the same inline observation, compression, and automatic GC controls as start.
agent-exec run [OPTIONS] -- <COMMAND> [ARGS...]Common options:
| Flag | Default | Description |
|---|---|---|
--timeout <SECONDS> |
0 |
Stop the process after this runtime; 0 disables the limit. |
--kill-after <SECONDS> |
0 |
Delay between SIGTERM and SIGKILL after timeout. |
--cwd <PATH> |
Inherited | Set the child current working directory. |
--env KEY=VALUE |
None | Set an environment variable; repeatable. |
--env-file <FILE> |
None | Load environment variables from a file; repeatable. |
--no-inherit-env |
false |
Do not inherit the launcher environment. |
--mask <KEY> |
None | Mask the named --env value in display metadata; repeatable. |
--stdin <VALUE> |
None | Provide input directly; --stdin - reads noninteractive caller input. |
--stdin-file <PATH> |
None | Copy file content to job-local input. |
--stdin-max-bytes <BYTES> |
64 MiB | Limit materialized input size. |
| `--wait [true | false]` | true |
--until <SECONDS> |
10 |
Bound inline observation. |
--forever |
false |
Observe until the job becomes terminal. |
--no-wait |
false |
Return without observation. |
--max-bytes <BYTES> |
65536 |
Limit the head excerpt from each stream. |
--tag <TAG> |
None | Assign a tag; repeatable and deduplicated. |
--notify-command <COMMAND> |
None | Run a shell command when the job finishes. |
--notify-file <PATH> |
None | Append a job.finished NDJSON event. |
--config <PATH> |
XDG default | Load a specific config.toml. |
--shell-wrapper <PROGRAM AND FLAGS> |
Config or platform default | Override the shell wrapper. |
--compress <MODE> |
Config or route |
Select inline compression. |
Input examples:
printf 'abc' | agent-exec run --stdin - -- cat
agent-exec run --stdin - -- cat <<'EOF'
line1
line2
EOF
agent-exec run --stdin "abc" -- cat
agent-exec run --stdin-file ./input.txt -- catIf --stdin - receives a terminal instead of redirected input, the command fails with error.code set to stdin_required.
agent-exec status <JOB_ID>The response can report created, running, exited, killed, or failed. It always includes job_id, state, and created_at; it includes started_at, finished_at, and exit_code when available.
agent-exec tail [--tail-lines <N>] [--max-bytes <N>] [--compress <MODE>] <JOB_ID>The response includes bounded stdout and stderr tails, their raw byte ranges and totals, encoding, and both log paths. Defaults are 50 lines and 65,536 bytes per stream.
agent-exec wait [--until <SECONDS> | --forever] [--poll <SECONDS>] <JOB_ID>The default client-side deadline is 30 seconds. Every response includes bounded stdout and stderr, byte ranges and totals, and utf-8-lossy encoding; terminal responses also include the exit code. Reaching the deadline does not stop the job. Use tail for later or repeated log retrieval, and run --timeout to limit process runtime.
agent-exec kill [--signal <NAME>] [--no-wait] <JOB_ID>The default signal is TERM. By default, kill briefly observes the result; --no-wait skips that observation.
agent-exec list [--state <STATE>] [--limit <N>] [--cwd <PATH> | --all] [--tag <PATTERN>]...By default, list returns jobs whose persisted current working directory matches the caller's current working directory. --cwd selects another directory, and --all disables current working directory filtering. States are created, running, exited, killed, failed, and unknown.
Repeated --tag filters use logical AND. An exact pattern such as ci matches that tag only. A namespace pattern such as project.build.* matches tags below that namespace.
agent-exec list --all --tag ci
agent-exec list --all --tag project.build.*
agent-exec list --tag ci --tag releaseagent-exec ps [--limit <N>] [--cwd <PATH> | --all] [--tag <PATTERN>]...ps is equivalent to list --state running and returns the same type set to list.
agent-exec tag set <JOB_ID> [--tag <TAG>]...The command replaces all tags, preserving the first occurrence of each duplicate. Omit --tag to clear the list.
agent-exec run --tag project.build --tag ci -- make build
agent-exec tag set 7f3a9c1e4b2d8a605e7c9f0134ab6d82 --tag project.release --tag approved
agent-exec tag set 7f3a9c1e4b2d8a605e7c9f0134ab6d82Stored tags use dot-separated segments containing alphanumeric characters and hyphens, such as ci, project.build, and release.v2. The .* suffix is reserved for filter patterns.
agent-exec notify set <JOB_ID> [OPTIONS]This metadata-only command updates the persisted notification configuration. It never invokes a sink immediately, including for a terminal job. Unspecified fields are preserved. --command replaces notify_command, while notify_file remains unchanged.
Completion notification:
JOB=$(agent-exec run --no-wait -- sleep 5 | jq -r .job_id)
agent-exec notify set "$JOB" --command 'cat > /tmp/event.json'Output-match notification:
JOB=$(agent-exec run --no-wait -- sh -c 'sleep 1; echo ERROR foo' | jq -r .job_id)
agent-exec notify set "$JOB" \
--output-pattern 'ERROR' \
--output-command 'cat >> /tmp/matches.ndjson'
agent-exec notify set "$JOB" \
--output-pattern '^ERR' \
--output-match-type regex \
--output-stream stderr \
--output-file /tmp/stderr-matches.ndjsonOutput-match settings apply only to lines observed after the configuration becomes active. Use agent-exec notify set --help for all fields.
agent-exec gc [--older-than <DURATION>] [--max-jobs <N>] [--max-bytes <BYTES>] [--dry-run]gc scans the entire jobs root, regardless of the current working directory. In the current implementation, it first builds an oldest-first pool of terminal jobs older than --older-than; --max-jobs and --max-bytes apply only within that pool and never select newer terminal jobs. Jobs in created or running state and jobs with unreadable state are preserved.
Selection is order-dependent: age eligibility is established first, --max-jobs removes age-only selection from the newest N pool entries, and --max-bytes may then select oldest pool entries until their removal would bring pool storage within the limit.
| Flag | Default | Description |
|---|---|---|
--older-than <DURATION> |
30d |
Build the age-eligible terminal pool. Accepted suffixes include d, h, m, and s. |
--max-jobs <N> |
None | Apply count policy within the age-eligible pool. |
--max-bytes <BYTES> |
None | Apply oldest-first byte policy within the age-eligible pool. |
--dry-run |
false |
Report aggregate effects without deleting directories. |
The age timestamp is finished_at when present, otherwise updated_at.
agent-exec gc --dry-run
agent-exec gc --older-than 7d --dry-run
agent-exec gc --older-than 7d
agent-exec --root /tmp/jobs gc --older-than 7dThe gc response is aggregate-only and has no jobs array:
| Field | Description |
|---|---|
root |
Resolved jobs root. |
dry_run |
Whether deletion was disabled. |
older_than |
Effective retention duration. |
older_than_source |
default or flag. |
deleted |
Directories actually deleted; always 0 for a dry run. |
skipped |
Total skipped directories; equal to out_of_scope + failed. |
out_of_scope |
Entries excluded by state, age, timestamp, readability, or policy limits. |
failed |
Eligible entries that could not be deleted or remained after deletion. |
freed_bytes |
Bytes removed, or bytes that a dry run would remove. |
scanned_dirs |
Directories scanned. |
candidate_count |
Directories selected by policy before deletion limits. |
agent-exec delete <JOB_ID> [--dry-run]
agent-exec delete --all [--dry-run]rm is a visible alias and returns the same response shape.
Single-job deletion is not scoped by the current working directory. The implementation rejects a job whose readable state is running. It may delete created, terminal, or unknown jobs, including a directory whose state is missing or unreadable. Inspect the job or use --dry-run before deleting an explicit ID when state integrity is uncertain.
delete --all removes only terminal jobs whose persisted current working directory matches the caller's current working directory. It skips created, running, unreadable-state, and terminal jobs whose recorded PID is still alive. Jobs from other directories contribute to out_of_scope but are not listed individually.
agent-exec delete 7f3a9c1e4b2d8a605e7c9f0134ab6d82
agent-exec delete --all --dry-run
agent-exec delete --all
agent-exec --root /tmp/jobs delete --allThe response includes root, dry_run, deleted, skipped, out_of_scope, failed, and per-job jobs. cwd_scope is present only for --all. Each job result contains job_id, state, action, and reason; action is deleted, would_delete, or skipped. A reported deleted action means the path was confirmed absent after deletion.
agent-exec schemaThe response contains the JSON Schema document for CLI response types, its schema format, and generation timestamp.
After a successful launch, run, start, and restart perform bounded, best-effort automatic GC by default.
- Default retention is
30d. created,running, and unreadable-state jobs are preserved.- Cleanup failure does not fail the launch command.
- Scan and deletion limits bound launch-time work.
Per-invocation controls are --no-auto-gc, --auto-gc-older-than, --auto-gc-max-jobs, and --auto-gc-max-bytes.
[gc]
auto = true
older_than = "30d"
max_jobs = 200
max_bytes = 1073741824
scan_limit = 200
delete_limit = 20CLI values override configuration for that invocation.
agent-exec serve exposes job operations to HTTP clients.
agent-exec serve [--bind <HOST:PORT> | --port <PORT>] [--allow-origin <ORIGIN>]The default address is 127.0.0.1:19263. --port changes the loopback port. A non-loopback bind requires both --insecure and a nonempty AGENT_EXEC_SERVE_TOKEN.
AGENT_EXEC_SERVE_TOKEN enables bearer authentication only for the mutating endpoints POST /exec and POST /kill/{id}. It does not protect GET /health, GET /status/{id}, GET /tail/{id}, or GET /wait/{id}. Those read endpoints can expose job state and output.
Keep the default loopback bind unless remote access is required. For non-loopback access, restrict the port with a firewall, private network, or authenticating reverse proxy. Do not expose the server directly to the public internet. The required --insecure flag acknowledges that the built-in token does not secure read endpoints.
--allow-origin enables CORS for one explicit origin. The wildcard origin * is rejected.
| Method | Path | CLI equivalent | Behavior |
|---|---|---|---|
GET |
/health |
None | Returns schema_version, ok, and type set to health. |
POST |
/exec |
run |
Starts a job and returns RunData. |
GET |
/status/{id} |
status |
Returns job status. |
GET |
/tail/{id} |
tail |
Returns bounded stdout and stderr tails. |
GET |
/wait/{id} |
wait --forever |
Blocks until a terminal state and returns bounded stdout/stderr output metadata. |
POST |
/kill/{id} |
kill |
Sends TERM; ?no_wait=true skips observation. |
HTTP responses use the same schema_version, ok, and type envelope fields as CLI responses.
{
"command": ["bash", "-c", "echo hello"],
"cwd": "/tmp",
"env": {"FOO": "bar"},
"timeout": 30,
"wait": true,
"until": 10,
"max_bytes": 65536
}Only command is required. Pass timeout as a nonnegative number of seconds; it may be fractional. until must be a nonnegative integer number of seconds. wait defaults to true, until to 10, and max_bytes to 65536. The obsolete timeout_ms field is rejected.
Start the host server on a non-loopback address with a strong token:
export AGENT_EXEC_SERVE_TOKEN="$(openssl rand -hex 32)"
agent-exec serve --bind 0.0.0.0:19263 --insecureProvide the same token to the container through its secret or environment configuration. From a Docker container on a platform that supports host.docker.internal:
curl -sS \
-H "Authorization: Bearer $AGENT_EXEC_SERVE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"command":["my-agent-script"],"wait":false,"until":0,"max_bytes":65536}' \
http://host.docker.internal:19263/execUse the returned job ID with the read endpoints. Because read endpoints do not require the bearer token, network restrictions remain mandatory.
agent-exec mcp exposes the managed-job lifecycle over stdio. It uses the same jobs root, persisted metadata, detached supervisor, logs, and response envelopes as the CLI; it does not require the HTTP server.
For tested setup instructions for Claude Code, Codex CLI, OpenCode, and Hermes Agent, see AI agent integrations.
Configure an MCP client to launch:
command: agent-exec
args: ["mcp"]
Set observation limits in the MCP server process environment:
AGENT_EXEC_MCP_DEFAULT_UNTIL_SECONDS=10
AGENT_EXEC_MCP_MAX_UNTIL_SECONDS=55
AGENT_EXEC_MCP_DEFAULT_UNTIL_SECONDS supplies an omitted until for MCP run and wait. AGENT_EXEC_MCP_MAX_UNTIL_SECONDS caps every MCP observation duration. Set the maximum below the MCP client's request timeout. An over-limit request uses the cap; it does not fail or stop the job.
Use a nondefault jobs root only when needed:
command: agent-exec
args: ["--root", "/path/to/jobs", "mcp"]
mcp_servers:
agent-exec:
command: agent-exec
args: ["mcp"]
env:
AGENT_EXEC_MCP_DEFAULT_UNTIL_SECONDS: "10"
AGENT_EXEC_MCP_MAX_UNTIL_SECONDS: "55"When MCP is unavailable, use agent-exec run -- <command> with CLI observation commands.
| Tool | Parameters | Behavior |
|---|---|---|
run |
command: string[], cwd?: string, env?: object, timeout?: integer, until?: integer |
Starts a detached job. timeout and until are seconds; the legacy omitted until is 10 seconds unless configured. |
status |
job_id: string |
Returns canonical job status. |
tail |
job_id: string, lines?: integer, max_bytes?: integer |
Reads bounded tails; defaults are 50 lines and 65,536 bytes. |
wait |
job_id: string, until?: integer |
Observes for a bounded duration and returns bounded stdout/stderr output metadata; the legacy omitted until is 30 seconds unless configured. Indefinite MCP waits are not supported. |
kill |
job_id: string |
Sends TERM. |
Retain the job ID returned by run. Closing the MCP transport, reaching an observation deadline, receiving no output, or encountering a tool error does not stop the job. Use kill only for explicit cancellation.
The MCP run tool intentionally omits CLI-only input, masking, notification, tag, compression, and shell-wrapper controls.
For an MCP host with a 60-second request deadline, a maximum of 55 seconds leaves time for the response to return. The default can remain shorter, such as 10 seconds.
agent-exec reads optional [shell], [gc], and [compression] sections from $XDG_CONFIG_HOME/agent-exec/config.toml, which normally resolves to ~/.config/agent-exec/config.toml.
[shell]
unix = ["sh", "-lc"]
windows = ["cmd", "/C"]
[gc]
auto = true
older_than = "30d"
max_jobs = 200
max_bytes = 1073741824
scan_limit = 200
delete_limit = 20
[compression]
default = "route"All keys are optional. Shell values fall back to sh -lc on Unix-like systems and cmd /C on Windows. GC and compression values fall back to their built-in defaults.
--shell-wrapper <PROGRAM AND FLAGS>- The file selected by
--config <PATH>, when specified - The default XDG configuration file, only when
--configis omitted - The built-in platform wrapper
The number of command arguments after -- selects the launch mode:
| Mode | Example | Behavior |
|---|---|---|
| Shell string | agent-exec run -- "echo hi && ls" |
A single argument is passed unchanged to the shell wrapper. |
argv |
agent-exec run -- cargo test |
Two or more arguments use an exec "$@" handoff after the wrapper initializes the shell environment. |
The argv handoff replaces the wrapper with the target workload, so PID and lifecycle tracking align with the command. Prefer argv for routine commands and shell-string mode for actual shell expressions.
The configured wrapper also runs --notify-command. Notification delivery always uses shell-string mode.
agent-exec run --shell-wrapper "bash -lc" -- ./my_script.sh
agent-exec run --config /path/to/config.toml -- ./my_script.sh--notify-command and --notify-file deliver a job.finished event after a launched job reaches a terminal state.
--notify-commandruns a shell command through the configured wrapper and writes event JSON to its standard input.--notify-fileappends one NDJSON line.completion_event.jsonstores the event and sink delivery results in the job directory.- Delivery is best effort; sink failure does not change job state.
- Inspect
completion_event.json.delivery_resultswhen delivery success matters.
JOB=$(agent-exec run --notify-file /tmp/agent-exec-events.ndjson -- echo hello | jq -r .job_id)
agent-exec wait "$JOB"
agent-exec tail "$JOB"
JOB=$(agent-exec run --notify-command 'cat > /tmp/agent-exec-event.json' -- echo hello | jq -r .job_id)
agent-exec wait "$JOB"Command sinks receive these environment variables:
AGENT_EXEC_EVENT_PATH: persistedcompletion_event.jsonornotification_events.ndjsonpathAGENT_EXEC_JOB_ID: canonical job IDAGENT_EXEC_EVENT_TYPE:job.finishedorjob.output.matched
Example payload:
{
"schema_version": "0.1",
"event_type": "job.finished",
"job_id": "7f3a9c1e4b2d8a605e7c9f0134ab6d82",
"state": "exited",
"command": ["echo", "hello"],
"cwd": "/path/to/current-working-directory",
"started_at": "2026-07-19T12:00:00Z",
"finished_at": "2026-07-19T12:00:00Z",
"duration_ms": 12,
"exit_code": 0,
"stdout_log_path": "/jobs/7f3a9c1e4b2d8a605e7c9f0134ab6d82/stdout.log",
"stderr_log_path": "/jobs/7f3a9c1e4b2d8a605e7c9f0134ab6d82/stderr.log"
}For signal termination, state is killed, exit_code may be absent, and signal is present when available.
install-skills installs only the embedded agent-exec skill into .agents/skills/ or .claude/skills/ and updates the corresponding .skill-lock.json. It is not a general skill installer and does not accept external sources.
agent-exec install-skills
agent-exec install-skills --claude
agent-exec install-skills --claude --globalA completion callback can return the job ID and event path to the OpenClaw session that launched the work. The session can inspect the persisted event and logs before responding.
SESSION_ID="01bb09d5-6485-4a50-8d3b-3f6e80c61f9c"
REPLY_CHANNEL="telegram"
agent-exec run \
--notify-command "openclaw agent --deliver --reply-channel $REPLY_CHANNEL --session-id $SESSION_ID -m \"job_id=\$AGENT_EXEC_JOB_ID event_path=\$AGENT_EXEC_EVENT_PATH\"" \
-- ./scripts/run-heavy-task.shWhen both agents share a filesystem, sending job_id and event_path is more compact than embedding the full event JSON.
JOB=$(agent-exec run --no-wait -- ./scripts/run-heavy-task.sh | jq -r .job_id)
SESSION_ID="01bb09d5-6485-4a50-8d3b-3f6e80c61f9c"
REPLY_CHANNEL="telegram"
agent-exec notify set "$JOB" \
--command "openclaw agent --deliver --reply-channel $REPLY_CHANNEL --session-id $SESSION_ID -m \"job_id=\$AGENT_EXEC_JOB_ID event_path=\$AGENT_EXEC_EVENT_PATH\""notify set updates future delivery metadata and does not invoke the callback immediately.
agent-exec run \
--notify-file /var/lib/agent-exec/events.ndjson \
-- ./scripts/run-heavy-task.shA separate worker can process the NDJSON file, retry delivery, and route events without coupling that work to the supervisor.
Keep command sinks short, fast, and idempotent. Common failures include quoting errors, environment or PATH differences, nonzero downstream exits, and incorrect delivery targets. Use a checked-in helper or durable worker for substantial orchestration.
When output-match notification metadata is active, the supervisor evaluates newly observed lines from stdout, stderr, or either stream and emits job.output.matched for every match.
containsperforms substring matching.regexuses Rust regular-expression syntax.- Earlier output is not replayed after
notify set. - Sink failure is recorded in
notification_events.ndjsonand does not affect job state. completion_event.jsonremains reserved forjob.finisheddelivery results.
{
"schema_version": "0.1",
"event_type": "job.output.matched",
"job_id": "7f3a9c1e4b2d8a605e7c9f0134ab6d82",
"pattern": "ERROR",
"match_type": "contains",
"stream": "stdout",
"line": "ERROR: connection refused",
"stdout_log_path": "/jobs/7f3a9c1e4b2d8a605e7c9f0134ab6d82/stdout.log",
"stderr_log_path": "/jobs/7f3a9c1e4b2d8a605e7c9f0134ab6d82/stderr.log"
}Diagnostic logs use stderr only:
RUST_LOG=debug agent-exec run -- echo hello
agent-exec -v run -- echo hellocargo build
cargo test --all
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings