Skip to content

metastable-void/rexec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rexec

Command-execution aggregator for AI coding agents.

rexec runs a small per-user host that several coding agents (Claude Code, Codex, Gemini CLI, ...) can share. Agents call a thin client which forwards the command to the host; the host runs it inside a fresh PTY, streams raw output to the human's console with a one-line banner, sends ANSI-stripped output back to the calling agent, and journals every run to a JSONL transcript.

The design assumes one human supervisor and several agents working concurrently in the same project. They get a single, ordered, human-readable log of what was executed, and the supervisor can interrupt or replay anything after the fact.

Features

  • One shared console. Output from concurrent agents is serialised one command at a time, so the human's screen stays readable.
  • Sanitised output to agents. ANSI escape sequences are stripped and CR is normalised to LF — agents see clean text, not progress-bar redraws.
  • Live raw output to the human. The host console preserves ANSI colours and any TTY behaviour.
  • Fresh PTY per command. Each command runs as if from a small (80x24) terminal with sane termios.
  • JSONL transcripts. Every run is appended to ~/.rexec/YYYY-MM-DD-HH:MM:SS.jsonl and can be listed, printed, or followed live.
  • Cooperative abort. Clients send {"action":"abort"} automatically on any catchable termination (Ctrl-C, SIGTERM, panic, dropped connection); the host SIGTERMs the spawned process group then SIGKILLs after a brief grace.
  • No daemon manager. The host is just rexec --start-host in a terminal; ^C cleans it up. Single static binary, no Python in the build graph, builds cleanly for musl targets.

Install

From crates.io:

cargo install rexec

From source:

git clone https://github.com/metastable-void/rexec.git
cd rexec
cargo install --path .

Unix only. Tested on Linux (glibc and musl). BSDs and macOS should work.

Quick start

In one terminal — the human's view — start the host:

rexec --start-host

It prints the socket path and transcript file:

rexec host listening on /tmp/.rexec-1000
rexec transcript: ~/.rexec/2026-05-21-09:42:18.jsonl

From an agent (or any other shell), run commands through it:

rexec --whoami "Claude Code" --dir "$PWD" -- grep -v foo bar.txt

The host prints a banner and the command's raw output to its own console; the client receives the ANSI-stripped output on stdout and exits with the command's exit code.

CLI

rexec --help | -h
rexec --check-host | -c
rexec --start-host | -s
rexec --list <N>
rexec --print | -p [--follow | -f] <transcript-name>
rexec --mcp-stdio | -m --whoami <NAME>
rexec --whoami <NAME> --dir <DIR> [--env VAR=VAL ...] [--read-stdin] -- <command> [args...]

Run a command

rexec --whoami Codex --dir /path/to/repo --env RUST_LOG=debug -- cargo test --workspace
Flag Required Description
--whoami yes Identifier of the calling agent. Appears in the host banner and transcript.
--dir yes Working directory for the child. The host chdirs here.
--env no VAR=VAL pairs, repeatable. Added to (not replacing) the host's environment.
--read-stdin no Read the client's stdin to EOF (must be valid UTF-8) and forward it to the child. The host attaches a pipe to the child's fd 0 and closes it after writing, so the child sees a real EOF. Without this flag the child's stdin is the PTY slave and reads on it will block.
-- yes Separator; everything after is the command to execute.

argv[0] is resolved via PATH (execvp semantics). Output to stdout is the command's combined stdout+stderr, ANSI-stripped, CR-normalised. The client's exit code is:

Code Meaning
N The command's exit code.
128+N The command was killed by signal N.
127 Host not running (HOST NOT FOUND on stderr), command not found (<arg0>: not found on stderr), spawn failure, or a transport error.
2 CLI usage error.

Check whether the host is up

rexec --check-host

Prints HOST RUNNING (exit 0) or HOST NOT FOUND (exit 127).

Start the host

rexec --start-host

Foreground; ^C to stop. Refuses to start if another host already owns the per-user socket. On exit the socket file is removed.

List transcripts

rexec --list 10

Lists up to N most recent transcripts, newest first:

2026-05-21-09:42:18 commands=19
2026-05-20-17:03:55 commands=4

Print a transcript

rexec --print 2026-05-21-09:42:18
rexec --print --follow 2026-05-21-09:42:18

Renders the transcript in the same format the host prints to its console. --follow (-f) streams new entries as they arrive.

Run as a stdio MCP server

rexec --mcp-stdio --whoami "Claude Code"

Speaks the Model Context Protocol (MCP) over stdio. The agent launches rexec --mcp-stdio --whoami <NAME> as a subprocess; each tool call becomes a fresh client connection to the rexec host, identical to invoking rexec --whoami ... on the command line. --whoami is fixed for the session.

Two tools are exposed:

Tool Purpose
exec Run a command via the host. Arguments: dir (string, required), argv (array of strings, required), envs (array of "VAR=VAL" strings, optional), stdin (UTF-8 string, optional). Returns a JSON object with exit, output, and an optional error field; isError is set when the command exited non-zero or could not be found.
check_host Probes the per-user host. Returns "HOST RUNNING" or "HOST NOT FOUND".

The MCP server itself does no work other than forwarding — --start-host must still be running somewhere for exec calls to succeed.

Configuration example (Claude Code's mcp_servers block, similar shape for other MCP clients):

{
  "mcpServers": {
    "rexec": {
      "command": "rexec",
      "args": ["--mcp-stdio", "--whoami", "Claude Code"]
    }
  }
}

Architecture

The host owns a Unix domain socket at /tmp/.rexec-$UID (mode 0600, owner only). Clients open a fresh connection per command — there is no persistent client state.

+----------------+              +---------------------------+              +---------------+
|  agent / shell |              |          host             |    forkpty   |    child      |
|  rexec client  | ---JSONL---> |  accept; per-conn worker  | -----------> | (fresh PTY)   |
|                | <--JSONL---- |  PTY -> host stdout (raw) |              +---------------+
+----------------+              |  PTY -> client (filtered) |
                                |  append transcript line   |
                                +---------------------------+
  • Concurrency vs. ordering. Commands run concurrently, but printing to the host console is serialised. The Nth-arriving request gets sequence number N and waits its turn to print the banner and output. Each client sees its own command's output independently, so a slow command never blocks a fast one from completing on the client side.
  • PTY. Each command runs under a fresh 80x24 PTY with sane termios (B38400, CS8, no input/output processing). This gives realistic TTY behaviour for tools that detect a terminal, without leaking the host's controlling terminal.
  • Environment. The child inherits the host's environment, with anything passed via --env added or overriding. HOME, PATH, etc. come from the host process unless the request supplies them.
  • Filtering. Output sent back to the client passes through an ANSI-stripping filter: CSI sequences, OSC strings (terminated by BEL or ST), and single-character ESC sequences are removed; CR becomes LF so redraws appear as separate lines. The host's own console sees the raw PTY bytes.

Protocol

The client opens a fresh socket per command and exchanges JSONL — one JSON object per line — with the host.

The first line of every connection is one of:

  • a Request (run a command — no "action" field), or
  • a Ping action ({"action":"ping"}), to which the host replies {"result":"pong"} and closes. This is what --check-host sends.

After a Request, the client may send further JSONL action lines (currently only Abort) on the same connection.

1. Request (client → host)

The first line is the request:

{"whoami":"Claude Code","dir":"/path/to/repo","envs":{"RUST_LOG":"debug"},"exec":["grep","-v","foo","bar.txt"]}
Field Type Description
whoami string Identifier of the calling agent.
dir string Working directory; the host chdirs the child here.
envs object<string,string> Environment variables added to the child. Omittable.
exec array argv[0] is the program (resolved via PATH); rest are arguments. Must be non-empty.
stdin string (optional) If present, the host attaches a pipe to the child's fd 0, writes these bytes (UTF-8), and closes the write end so the child sees EOF. If absent, the child's stdin is the PTY slave and reads on it block.

2. Response (host → client)

The host writes one line back when the command completes:

{"exit":0,"output":"foobar\n"}
Field Type Description
exit int Exit code. 128+N if the child was killed by signal N; 127 if not found or spawn failed.
output string Filtered combined stdout+stderr (ANSI-stripped, CR→LF).
error string (optional) Tag describing why the run did not complete normally. See below.

error values currently defined:

Tag Meaning
not_found execvp reported ENOENT (or similar) for argv[0].
spawn_failed chdir, setenv, or fork failed before exec.
aborted The host killed the child because the client sent abort or disconnected.

3. Ping / Pong (client ↔ host)

Sent as the first (and only) message on a connection used purely to probe the host:

{"action":"ping"}

The host replies:

{"result":"pong"}

and closes the connection. --check-host uses this. A successful connect to the socket alone is also accepted as proof that the host is running, so new clients still report HOST RUNNING against older hosts that don't know the ping action.

4. Abort (client → host, optional)

At any point after the request, the client may send:

{"action":"abort"}

The reference client sends this automatically on any catchable termination (SIGINT, SIGTERM, SIGHUP, panic, or any drop of the connection before the response is read). On receipt the host signals the child's process group with SIGTERM, then SIGKILL after a 200 ms grace, and tags the transcript entry with "error":"aborted". Clients that don't implement abort remain fully compatible — the host treats EOF on the connection identically.

Host console output

Per command, the host prints:

[2026-05-21T09:42:18Z] Claude Code:/path/to/repo $ grep -v foo bar.txt
foobar
                                  <- trailing blank line separates commands

Output between the banner and the trailing blank line is the raw PTY stream, including any ANSI colour and cursor control the command produced.

Transcript format

~/.rexec/YYYY-MM-DD-HH:MM:SS.jsonl is JSONL with one object per executed command, in arrival order:

{"whoami":"Claude Code","dir":"/path/to/repo","envs":{},"exec":["grep","-v","foo","bar.txt"],"exit":0,"output":"foobar\n","time":"2026-05-21T09:42:18Z"}
{"whoami":"Codex","dir":"/path/to/repo","envs":{},"exec":["id","-un"],"exit":0,"output":"alice\n","time":"2026-05-21T09:42:24Z"}

The file is opened with O_CREAT | O_EXCL; the host refuses to start if a transcript with the same name already exists. Entries are flushed after every append, so the transcript is durable up to the last completed command.

Security notes

  • The socket is created at mode 0600 and lives in /tmp/.rexec-$UID, i.e. it is accessible only to the owning user. Anyone with that user's privileges can run arbitrary commands through the host; treat the host as equivalent to a shell running as you.
  • The host does not authenticate clients beyond filesystem permissions on the socket. Do not start a host as root unless you want any process running as that user to be able to execute anything.
  • Output from the child is rendered raw on the host console. A hostile command can emit terminal escape sequences against the human's terminal; this matches bash's default behaviour and is preserved deliberately so the human sees what the command actually produced.

License

Dual-licensed under either of:

at your option.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MPL-2.0 licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MPL-2.0
LICENSE-MPL

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages