A cross-platform C tool that lets an AI agent execute terminal commands through a human approval gate. The agent requests a command, the operator sees exactly what will run, and nothing executes until the operator types y.
When an AI agent has tool-call access to run shell commands, every invocation is a potential risk. This tool sits between the agent and the operating system. It prints the full command, waits for explicit approval, and logs every request regardless of whether it was approved or rejected. The agent never runs anything silently.
- The agent (or any caller) invokes
runwith a command - The tool prints the command to stderr and prompts for approval
- The operator reads the command and types
yto approve or anything else to reject - If approved, the command runs via the system shell and the exit code is captured
- A structured JSON result is printed to stdout for the caller to parse
- If a log file is configured, every request is recorded with a timestamp
No dependencies. Compiles with any C compiler on any platform.
macOS / Linux:
cd cmds
gcc -O2 -o run run.cmacOS (if headers are not found):
cc -O2 -o run run.c --sysroot="$(xcrun --show-sdk-path)"Windows (MSVC):
cl run.c /Fe:run.exe
Windows (MinGW):
gcc -O2 -o run.exe run.c
./run ls -la /tmpThe tool assembles the arguments into a single command string, shows the approval prompt, and runs it if approved.
./run "git status && git log --oneline -5"echo '{"cmd":"uname -a"}' | ./run --jsonReads a JSON object from stdin with a "cmd" field. The approval prompt still appears on the terminal because it reads from /dev/tty (Unix) or CON (Windows), not stdin.
| Flag | Description |
|---|---|
--json |
Read the command from a JSON object on stdin instead of argv |
--yes |
Skip the approval prompt entirely (use only in trusted, locked-down pipelines) |
--timeout N |
Seconds to wait for operator input (0 = wait forever, which is the default) |
--log FILE |
Append every request to FILE with a timestamp and approval status |
--help |
Print usage information |
| Code | Meaning |
|---|---|
| 0 | Command executed and returned exit code 0 |
| 1 | Command executed but returned a non-zero exit code |
| 2 | Operator rejected the command |
| 3 | Usage error (no command provided, bad JSON, etc.) |
The tool prints a JSON line to stdout after every invocation so the calling agent can parse the result programmatically.
Approved and executed:
{"status":"executed","exit_code":0,"command":"ls -la"}Rejected by operator:
{"status":"rejected","command":"rm -rf /"}The command's own stdout and stderr pass through to the terminal normally. The structured JSON result is a separate line printed after execution completes.
When --log is specified, every request is appended to the file with this format:
[2026-02-17 14:32:01] APPROVED | git status
[2026-02-17 14:32:01] SUCCESS | git status
[2026-02-17 14:33:15] REJECTED | rm -rf /
Each request produces two log entries when approved (APPROVED then SUCCESS or FAILED) and one entry when rejected (REJECTED).
The approval gate is the core safety mechanism. Here is how it works:
- The command is displayed in full on stderr so the operator can inspect it
- The prompt reads from the controlling terminal (
/dev/ttyon Unix,CONon Windows), not from stdin -- this means JSON mode and piped input do not bypass the prompt - Only
yorY(optionally followed by other characters) counts as approval - An empty line,
n,N, EOF, or any other input counts as rejection - If the terminal is not available and
--yeswas not passed, the command is rejected by default
The --yes flag disables the gate entirely. It exists for CI/CD or other environments where a human has already vetted the command set. Do not use it in interactive agent sessions.
Define a tool that shells out to run:
{
"name": "run_command",
"description": "Execute a shell command on the host. Requires operator approval.",
"parameters": {
"type": "object",
"properties": {
"cmd": {
"type": "string",
"description": "The shell command to execute"
}
},
"required": ["cmd"]
}
}The agent sends {"cmd": "..."}, your orchestrator pipes it to ./run --json, and the operator approves or rejects.
Any system that can spawn a process can call run directly:
./run --log agent.log git push origin main
The log file provides an audit trail of everything the agent attempted.
- The tool does not sanitize or filter commands. The operator is the filter.
- The
--yesflag removes the only safety gate. Use it with extreme caution. - The log file is append-only from the tool's perspective but has no file locking. In single-agent scenarios this is fine.
- The JSON parser is minimal and does not handle nested objects or arrays. The
"cmd"value must be a simple string. - Commands run with the same privileges as the user who started the tool. There is no sandboxing.
MIT