agent-shell: stop spilling your outputs
since the beginning of time, ai agents have wanted one thing at one thing only: redirecting stderr to stdout then piping the output to head or tail or grep or sed or awk. That’s all they really want.
Except, they do it wrong the majority of the time (because “ai labs” are fundamentally incompetent at software engineering, as we’ve previously discussed before).
Common ones I see routinely which are painful and require manual “reminders” into the agent to stop:
uv run my-big-honking-test 2>&1 | grep -E "failed:|error:" | head -5- but then the test fails with a non-zero error code, the
grepreports an error count, so then the agent RE-RUNS THE ENTIRE TEST with a different grep failure to “find the failing test case”- except, the failing test case grep requires “guessing the future” regex, but the first attempt at “guessing the error regex” fails, then code agent continues re-running a 20 minute test suite with different grep filters until you remind it: “FILES EXIST! WE HAVE A FILE SYSTEM! WRITE OUTPUT TO FILES THEN READ THE FILES TO SEE THE ERRORS!”
- also, since current ai code agents have no feeling for “time” or “effort” they don’t care about running a 30 minute program-stderr-to-stdout-to-pipe-grep 10 times in a row even if each attempt takes 30 minutes.
- but then the test fails with a non-zero error code, the
- and a more toxic approach is where models decide to use live public
URLs for reference material then
curl | grepexcept, again, models try to “guess grep filters”, fail, then re-run thecurla dozen times with different “guess the regex” filters instead of saving the content locally and then searching through it (I often see claude get rate limited or blocked by upstream services because it “curls and greps” the same URLs in a loop, and you have to remind it every time “hey, files exist? you know? use files?”)
I’ve run some bulk analysis about how often models “do the wrong
thing” or “waste time guessing” (instead of following more proof-based
data practices) over my claude session history (you do know all your
claude session and all commands and tool calls are saved at
~/.claude right?) and here’s results:
ways models try to read things
| Pattern | Count | Share of Bash commands |
|---|---|---|
Piped into a filter
(grep/head/tail/awk/sed/etc.) |
123,637 | 62.8% |
| Bare narrowing command, no pipe | 22,963 | 11.7% |
| – | ||
| Any pipe at all | 135,413 | 68.8% |
Bare full-file read (cat) |
1,573 | 0.8% |
| Redirect-to-file, later read in full (“save then read”) | 4,301 | — |
common filter patterns guesses:
| Shape | Count | Likely context |
|---|---|---|
sed (bare, e.g. sed -n 'N,Mp' file) |
15,123 | line-range paging |
grep | head |
9,497 | search, then cap the results |
cd | head |
5,998 | cd dir && cmd \| head |
grep (bare) |
4,523 | direct search on a file |
uv | tail |
3,864 | tailing output of a uv run/uv pip
command |
cd | tail |
3,253 | cd dir && cmd \| tail |
tail (bare) |
2,764 | log/output tailing |
ls | head |
1,699 | capped directory listing |
find | head |
1,365 | capped file search |
common slop guess counts:
| Metric | Count |
|---|---|
| Guess-then-give-up-and-read episodes (≥2 narrow attempts, then a full read) | 541 |
| Unresolved thrash episodes (≥3 narrow attempts, never followed by a full read) | 2,701 |
| Total narrow commands spent inside these episodes | 5,006 |
Resolved episodes closed by the Read tool |
447 (83%) |
Resolved episodes closed by a bare cat |
94 (17%) |
multiple
different cmd | filter guesses in a row
here are full attempt-count distribution for episodes
eventually resolving into a full read (example: running a
command with a failed filter, then continuing to run
cmd | filterA -> cmd | filterB over and
over again until a “filter” matches instead of just saving output to a
file and reading directly):
| Attempts before giving up | Episodes | Share of resolved episodes |
|---|---|---|
| 2 | 233 | 43.1% |
| 3 | 101 | 18.7% |
| 4 | 56 | 10.4% |
| 5 | 30 | 5.5% |
| 6 | 23 | 4.3% |
| 7 | 22 | 4.1% |
| 8 | 14 | 2.6% |
| 9 | 12 | 2.2% |
| 10–20 | 27 | 5.0% |
| 21+ | 23 (max: 342 attempts on one target) | 4.3% |
Agent Shell
So what fixes it?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Language Files Lines Code Comments Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JSON 8 354 354 0 0
Python 99 25838 22747 9 3082
Shell 3 75 55 12 8
TOML 1 85 71 0 14
─────────────────────────────────────────────────────────────────────────────────
Markdown 33 8737 0 6985 1752
|- JSON 5 147 147 0 0
|- Python 3 296 283 0 13
(Total) 9180 430 6985 1765
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Firstly, you must understand all these failed “output discovery” patterns is not one of those wacky and mysterious spooky-action-at-a-distance “emergent AI behaviors” – these failed patterns are DIRECTLY programmed into the models via incompetent over-compensated junior experience developers not knowing how to build or grow maintainable and observable systems.
These patterns are being burned into model behavior by children who
have never even read or used a dup2() system call, yet they
are trying to create “agent harness RL behavior feedback loops”
resulting in un-grounding of the entire global software development
profession in regressive “everything is a shell script just
guess-and-go” backwards logic we have been fighting against for the past
30 years.
take these load-bearing byte-identical truths why don’t you:
- ai agent coders hate the unix concept of “stderr vs stdout” they always obsessively “2>&1” EVERYTHING they run
- ai agent coders are TERRIFIED of seeing output they don’t control. Almost EVERY command run gets a length or content filter to “hide the actual output” and only “extract what we try to forward-guess where the signal is in the pipe line-delimited byte stream fields”
except agents are wrong on those cases all the time (see: stats above).
So, I tried to think of a better way. (and again, yeah, i’m just giving free work to trillion dollar companies because they can’t do it themselves apparently)
What if instead of every command outputting to stdout/stderr, commands ONLY write to files? With full run/performance manifest audits too? Then agents never have to be afraid of having to accidentially pollute their context or over-charge unwanted input tokens (if “input token cost” wasn’t such a huge concern, every tool call could just be a forked sub agent to validate/return full results all the time instead of this “micro guess grep filters and fail often” pattern every lab has settled on).
Here, the agent-shell
(ash) is runnable via uv run ash or other
scripts/ash runners:
$ uv run ash uv run pytest
✓ state=completed exit=code:0 duration=1.842s cpu=1.495s max-rss=84.2MiB
time: start=2026-08-28T14:03:12.417Z end=2026-08-28T14:03:14.259Z user=1.211s system=284ms cpu=81.2%
host: load=2.10/1.92/1.70->2.08/1.91/1.70 memory-available=18.2GiB->18.1GiB memory-used=42.5%->42.6% processes=312->311
artifacts: /workspace/.agent-shell/artifacts/uv/4f92a761cc73/run_20260828T140312.417000Z-a1b2c3d4
storage: streams=2/2 logical=186.4KiB new=186.4KiB reused=0B complete=yes
changes: observed=no
next: ashctl show a1b2c3d4 mergedSo, here: uv run ash wrapped uv run pytest
and all the output for stdout and stderr and other artifact details
showed up with this structure in the artifacts/
directory:
events.jsonl receipt.json stderr.bin stdout.bin
The events.jsonl saves a running stream of metadata
details about the run including:
- cpu and memory usage for the launched process every couple of milliseconds
- byte progress of stdout and stderr files over time (so they can be re-merged into a unified output stream if requested later)
- then when it’s all done the above stats are returned for overall progress and system usage and duration and min/max resource usage
The weird thing about running a command under the ash
system is, well, it gives you NO output until the final artifact
metadata trailer when everything is complete (you could of course
live-tail the stdout files in the artifact directory etc)
BUT THATS NOT ALL!
The simplest usage is just
ash <cmd with args to wrap for stdout/stderr saving>
but there is a more advanced ashctl command which allows to
manage logical or physical pipe processes within the
agent-shell system but with each step potentially saved to
files first (like the “bad test run, grep for error, error grep failed,
re-run test” doesn’t need to happen because the “bad test run” would be
saved first for re-use automatically; but these are optional depending
on the requirement, obviously you wouldn’t want to log an infinite
generator like yes to a file, but you would want your test
output logged if it failed etc).
Clearly, the REASON agents are so “stress-reinforced” to run
sometimes completely unhinged 5 to 30 part bash pipe shell abominations
(no excuse for EVER doing such things though: it just shows you are an
amatuer developer if you think “text processing through 16 dynamic pipe
filters without written reusabilty architecture” is ever acceptable
anywhere) is because each tool call turn of an agent is “expensive” – if
you can get results threaded through 8 commands in one tool
call, that saves 8 context intput loops, so “save one output to a file
always” doesn’t fix this problem, which is why we have a more advanced
ashctl mode which CAN run both saved outputs and filters or
pipes natively and safely with re-use abilty built in to avoid
re-running to find things again.
For a long-running command, opt into one immediate active-run handle on stderr, then use that canonical ID in exactly one later bounded wait call:
uv run ashctl --json run --announce --no-quickview -- SERVICE ARG...
uv run ashctl watch RUN_ID merged \
'listening on (?P<port>[0-9]+)' --until match --timeout-ms 60000 --format json--announce flushes one versioned
RunAnnouncement to stderr after child start; final
completion stdout remains one valid human/JSON document.
running remains the recovery/discovery surface when the
launch handle was not retained. The watcher returns
satisfied, run_exited, timed_out,
cancelled, or evidence_failed, along with
bounded matching lines and named regex captures. Cancelling the
read-only wait leaves the producer unchanged. It never mirrors the full
child stream or executes a trigger callback. An early live match is
marked provisional; replay after receipt finalization verifies the
source identity. See the live
monitoring cookbook.
For several ad hoc commands or one finite transformation, keep the entire run/review loop in one invocation:
uv run ashctl compose -- \
git status --short \
::then:: uv run pytest -q
uv run ashctl compose -- \
find tests -maxdepth 1 -type f -print \
::into:: sort \
::into:: tail -n 20
uv run ashctl compose -- \
yes \
::pipe:: head -n 20::then:: is ordered stop-on-failure sequencing.
::into:: completes a stage and replays its exact stdout
artifact as the next stdin. ::pipe:: runs a linear Unix
pipeline concurrently with kernel backpressure and ordinary
early-close/SIGPIPE behavior while teeing every stage into its own
receipt and split artifacts. Only bounded leaf/final output enters the
response.
Anyway, here’s the rest. Enjoy:
Agent Shell Intro Article
The terminal was built to print. Agents need receipts.
Introducing Agent Shell: quiet, artifact-first command execution for AI agents and the humans who operate them.
Most command-line tools assume that their output has one destination: the person watching a terminal right now. Standard output and standard error flow into the terminal, the terminal scrolls, and whatever survives in scrollback becomes the working record.
That model is wonderfully direct for a human typing occasional commands. It is much less comfortable for an AI agent operating through a bounded context window and a sequence of tool calls.
An agent does not need 4,000 passing test lines copied into its next prompt. It does need to know whether the test command succeeded, how long it took, whether it consumed unusual resources, where the complete output went, and how to ask a small follow-up question about it. If the command fails, the agent needs the useful diagnostic—not an arbitrary terminal-sized tail and not a vanished transcript.
This mismatch leads to a familiar defensive ritual:
some-command > /tmp/output.txt 2> /tmp/errors.txt
tail -n 50 /tmp/errors.txt
grep -nE 'failed|error' /tmp/output.txtThe ritual is repetitive, easy to get subtly wrong, and incomplete. It usually loses stdout/stderr chronology. It rarely records exact timing, environment, resource, or exit evidence. Temporary filenames collide or disappear. Large results still flood the caller if one redirection is forgotten. Every follow-up costs another agent/tool round trip.
Agent Shell starts from a different default:
A command’s primary result should be a durable, structured receipt—not terminal text.
The project is named agent-shell. Its simple command is
ash; its advanced control surface is
ashctl.
The idea in one command
Put ash in front of an ordinary command:
uv run ash uv run pytest -qThe child still receives its normal argument vector, working directory, environment, and piped stdin. Its stdout and stderr are captured rather than inherited by the terminal. The caller receives a compact completion like this:
✓ state=completed exit=code:0 duration=1.842s cpu=1.495s max-rss=84.2MiB
time: start=2026-08-28T14:03:12.417Z end=2026-08-28T14:03:14.259Z user=1.211s system=284ms cpu=81.2%
host: load=2.10/1.92/1.70->2.08/1.91/1.70 memory-available=18.2GiB->18.1GiB memory-used=42.5%->42.6% processes=312->311
artifacts: /workspace/.agent-shell/artifacts/uv/4f92a761cc73/run_20260828T140312.417000Z-a1b2c3d4
storage: streams=2/2 logical=186.4KiB new=186.4KiB reused=0B complete=yes
changes: observed=no
next: ashctl show a1b2c3d4 mergedThat response answers the control questions immediately:
- Did execution and capture complete?
- How did the child exit?
- When did it run, and for how long?
- What CPU, memory, and host conditions were observed?
- Where is the exact evidence?
- What is the shortest useful next action?
The artifact path is printed once in the completion. Stable filenames
beneath it—stdout.bin, stderr.bin,
receipt.json, and events.jsonl—do not need to
be repeated on every run. If captured, stdin is stdin.bin.
Optional and derived evidence has equally stable names.
Small output is the deliberate exception to quiet execution. If
combined stdout and stderr are readable UTF-8, nonempty, at most 10
logical lines, and at most 16 KiB, the entire timestamped result appears
once as a quickview. A tiny ls does not require a second
read; a large test or build does not consume the agent’s context.
Conceptually, the broker sits between execution and observation:
argv + cwd + env + stdin
|
v
Agent Shell ------> compact completion / bounded answer
|
+-----------> child process
|
`-----------> immutable run evidence
|- stdin.bin (when captured)
|- stdout.bin
|- stderr.bin
|- events.jsonl
`- receipt.jsonThe important change is not redirection by itself. It is the unification of execution, timing, evidence, retrieval, and lifecycle under one run identity.
Exact bytes and useful time
Process output is not fundamentally made of lines. Programs emit binary bytes, partial lines, carriage-return progress displays, and final fragments without a newline. Stdout and stderr are separate file descriptors; their absolute write order cannot always be recovered after the fact.
Agent Shell therefore stores the raw streams unchanged. Beside them it records an append-only event journal with monotonic elapsed time, stream identity, offset, and byte length for each observed chunk. Derived views can then present timestamped lines or the observed interleaving without corrupting the original evidence.
This separation supports both meanings of “exact”:
stdout.binandstderr.bincontain the exact captured bytes for each stream.events.jsonlrecords when the broker observed each chunk and allows a merged view in first-byte observation order.
Wall-clock UTC timestamps establish provenance. Duration and event offsets use a monotonic clock, so a wall-clock adjustment cannot make elapsed time run backward. The receipt also records user and system CPU and qualified RSS samples. Host load, available memory, and process counts are explicitly observations—not claims that the child caused the before/after difference.
Readable views are reconstructions over the evidence:
uv run ashctl show RUN_SUFFIX stdout --lines 120:180
uv run ashctl show RUN_SUFFIX merged --since-ms 2000 --until-ms 4000
uv run ashctl diagnostics RUN_SUFFIXThe raw evidence remains authoritative. Diagnostics and the line/time index are created lazily on first relevant access instead of slowing every successful command with work that may never be used.
One evidence model, several sizes of workflow
Agent Shell deliberately has a small simple interface and a separate advanced one. You choose the smallest surface that answers the current question:
| Need | Interface | Result |
|---|---|---|
| Run one command quietly | ash |
Receipt, artifacts, optional tiny quickview |
| Run with timeout or capture controls | ashctl run |
The same run contract with explicit options |
| Run and answer one known question | ashctl inspect |
Receipt plus one bounded native query |
| Revisit existing evidence | ashctl show/filter/project/bundle |
Bounded or exported evidence without rerunning |
| Run a few ad hoc stages | ashctl compose |
One bounded aggregate plus a receipt per stage |
| Run a reusable workflow | ashctl batch |
Durable typed specification, manifest, replay, and comparison |
| Wait for one live condition | ashctl watch |
Typed status, bounded matches, and extracted values |
ash remains option-free:
ash PROGRAM [ARG ...]. Only its exact --help
and --version invocations are reserved. Advanced execution
arguments belong on ashctl run, separated from the child by
--:
uv run ashctl run --timeout-ms 120000 --no-quickview -- cargo test --workspaceThis split matters. The common operation stays effortless, while advanced features remain discoverable without turning every child argument into a potential broker option.
When you already know the question
Suppose an agent runs a test suite and already knows that it only
needs failure, success, and coverage lines. A quiet run followed by a
separate filter would add an unnecessary run-ID feedback loop.
inspect fuses the two operations:
uv run ashctl inspect filter merged 'failed|error|passed|coverage' \
--ignore-case --limit 40 -- uv run pytest -qThe complete split streams and receipt are still finalized normally. Only the explicit bounded query enters the response. The result reports query time and total broker time separately. If more matches exist, it includes an opaque cursor and a directly runnable continuation over the existing run; it never silently reruns the child.
The native query family covers the recurring useful parts of
sed, grep, and awk without trying
to clone those languages:
# Select a logical range.
uv run ashctl select RUN_SUFFIX stdout --items 20:80
# Filter both streams in observed order.
uv run ashctl filter RUN_SUFFIX merged 'warning|error' --ignore-case
# Project typed fields rather than parsing receipt text.
uv run ashctl project RUN_SUFFIX receipt \
--field state --field exit.kind --field wall_msSources include stdin, stdout, stderr, merged output, events, diagnostics, receipt data, selected environment evidence, filesystem observations, and batch records. Item and byte limits remain explicit. For a portable handoff to a human or another system, one command can synthesize metadata plus a selected view:
uv run ashctl bundle RUN_SUFFIX \
--view interleaved --format markdown --output run-report.mdWhen one tool call needs several commands
Agents frequently combine unrelated checks into one shell call simply
to avoid several tool round trips. ashctl compose supports
the small set of patterns the project observed repeatedly:
# Ordered stop-on-failure checks.
uv run ashctl compose -- \
uv run pytest -q \
::then:: uv run ruff check . \
::then:: uv run mypy
# Complete one stage, then replay its exact stdout as the next stdin.
uv run ashctl compose -- \
find tests -maxdepth 1 -type f -print \
::into:: sort \
::into:: tail -n 20
# Stream concurrently and let the finite consumer stop the producer.
uv run ashctl compose -- yes ::pipe:: head -n 20::then:: is ordered sequencing. ::into:: is
sequential, materialized stdout-to-stdin dataflow: the producer
finishes, its output is finalized, and the exact artifact becomes the
consumer’s stdin. ::pipe:: is the native Unix counterpart:
stages overlap over a kernel pipe with backpressure and ordinary
early-close/SIGPIPE propagation.
Every stage receives its own normal receipt, stream files, event journal, and timing. Consumed intermediate output stays quiet. Only bounded leaf results enter the aggregate response. Underneath, composition compiles into the same typed batch contract used by reusable workflows, so recall, continuation, export, replay, and comparison work without a second execution system.
The syntax is intentionally not a mini shell. The parser examines
already separated argv tokens and recognizes only exact
::then::, ::into::, ::pipe::, and
::arg:: values. It never reparses a string with
shlex, performs expansion, or evaluates shell text.
::arg:: escapes one reserved token when that literal value
must reach a child.
When a long-running process must become ready
Long-running services introduce a different feedback loop: start a process, discover its artifact directory, repeatedly tail it, extract a value such as a port, and decide what to do next.
The launch side can opt into one immediate, flushed identity on stderr:
uv run ashctl --json run --announce --no-quickview -- SERVICE ARG...The final completion remains one valid JSON document on stdout. An
automation caller can read the announcement’s canonical
run_id while retaining ownership of the still-running
launch process. The announcement also gives the full run directory;
stdout.bin and stderr.bin already exist there
for direct tailing. Plain ash prints its completion after
the command ends. A second tool handle makes one bounded wait:
uv run ashctl watch RUN_ID merged \
'listening on (?P<port>[0-9]+)' \
--until match --timeout-ms 60000 --format jsonThe result contains a typed status, bounded matching records, and the
named port capture. Other conditions can wait for a count,
a quiet interval after a match, final exit, or match-or-exit.
Some services flush a prompt without ending the line. For output such
as port=4317>, enable partial-line matching:
uv run ashctl watch RUN_ID stdout 'port=(?P<port>[0-9]+)>' \
--partial --timeout-ms 60000 --format jsonThe closing > keeps the regex from accepting an
unfinished port number. The result includes the named capture, a raw
byte offset, and partial=true. Each logical line counts
once even if later chunks also match. The producer must flush its
output; a watcher cannot read bytes still buffered in the child.
watch is a read-only query, not a supervisor. Pressing
Ctrl-C cancels the wait and returns typed cancellation with the
conventional exit code; it does not signal or adopt the producer. A
match observed before finalization is labelled provisional. Repeating
the query after completion verifies current raw evidence against the
finalized receipt and reports the source identity.
That boundary is purposeful: the broker returns an observation or value, and the agent decides the next action. It does not hide policy inside callbacks.
Better productivity comes from fewer accidental decisions
The value of artifact-first execution compounds across an agent session.
First, output volume is no longer a surprise. Every default response
is bounded, and complete evidence is still available. An agent can run a
verbose compiler, test suite, package manager, or repository audit
without guessing whether it needs 2>&1,
tee, head, or a temporary file.
Second, the first response contains control-plane facts rather than an arbitrary slice of data-plane text. Exit status, duration, CPU, RSS confidence, host conditions, storage state, and the durable identity arrive in a consistent shape across commands.
Third, follow-up questions share one vocabulary. A human, an agent, and an automation process can all refer to the same immutable run ID. They can read exact files, request a bounded merged view, export a bundle, or project typed fields without inventing another convention.
Fourth, known questions collapse into one call. inspect
handles one command plus one evidence query; compose
handles finite ad hoc command sequences; watch handles one
live condition. These surfaces came from observed agent tool-call
patterns, not from an attempt to design a general replacement shell.
Finally, failed work becomes inspectable state rather than discarded terminal history. A nonzero child still has a receipt. Structured diagnostics, event timing, exact streams, selected environment context, and optional filesystem changes can be reviewed independently and compared with a later replay.
Safety through explicit boundaries and durable evidence
Agent Shell improves execution safety in the operational sense: fewer quoting mistakes, bounded observations, durable provenance, integrity checks, stable machine contracts, and explicit lifecycle results. It does not claim to be a sandbox, permission engine, container runtime, or operating-system policy layer.
That distinction matters:
- Native requests use argument vectors, not opaque command strings. Any glob, pipe, redirect, or substitution used by an outer shell happens before Agent Shell receives argv.
- The child inherits the caller’s cwd and environment and runs with the caller’s operating-system authority. Capturing an effect is not preventing it.
- Optional filesystem tracking records bounded before/after observations. It does not constrain writes or prove that the child caused every observed change.
- Host and process metrics are measurements with explicit unavailable states, not invented guarantees.
- Active watchers read append-only evidence and never execute trigger actions.
- Derived indexes and diagnostics can be rebuilt only from receipt-verified raw sources; they are acceleration and presentation state, not authority.
Receipts store the command argument vector, so do not put secrets in
command-line arguments. Children inherit the full environment, but
environment evidence is not persisted unless specific names are
requested with --record-env; never select secret-bearing
names. Treat the artifact root itself as potentially sensitive execution
evidence and protect it accordingly.
For automation, ASH_FMT=json gives the simple runner’s
machine envelope and global ashctl --json selects versioned
advanced results. Unknown fields in typed requests are rejected.
Published JSON Schemas let integrations validate the exact contracts
they persist:
ASH_FMT=json uv run ash command
uv run ashctl schema run-spec --output run-spec.schema.json
uv run ashctl schema receipt --output receipt.schema.json
uv run ashctl schema all --output schemasHuman output and JSON are rendered from the same immutable completion-data model, preventing the two interfaces from growing separate interpretations of a run.
A self-managing artifact store
Durable evidence is useful only if it does not create an infinite directory of forgotten logs.
Before a run can reach retention, one combined active stdout/stderr
budget protects the disk from an infinite logger, and an independent
captured-stdin budget protects it from an unbounded producer. Both
default to 256 MiB and are configurable. Exhaustion finalizes a truthful
output_limited or input_limited receipt
containing the exact stored—and for stdin, child-visible— prefix rather
than silently truncating and claiming success.
By default, successful execution applies a 1,000-run and 5 GiB
logical-byte LRU policy. The newest run and active runs are protected;
important finalized runs can be pinned. Each executable plus normalized
argv group has a sibling latest directory symlink for
convenient direct traversal, while immutable run IDs remain the
authoritative identity. Retention repoints or removes
latest when older runs expire.
Concurrent agents coordinate through short-lived lifecycle leases. A run being published, rendered, watched, or directly queried is skipped by retention; close, pin, recovery, and deletion revalidate under exclusive ownership. Receipt paths are also resolved through one run-contained, symlink-refusing boundary before any managed read or lazy derived write.
Finalized discovery also binds every plain receipt or manifest to its
canonical directory identity, so a linked, copied, or edited marker
cannot quietly become another addressable run. Lifecycle responses
follow the same context discipline as execution: GC prints counts by
default, --details adds one shared root plus relative
deletions, and JSON pin/retention operations expose published typed
contracts. Exact artifact exports are complete-before-replace,
preserving an existing destination when reading or transparent gzip
decoding fails.
Exports are deliberately external to the evidence store. Before any
command runs or output parent is created, ashctl rejects an
--output path that is lexically inside the managed root or
currently resolves there through a symlink. That keeps a convenient
recall command from replacing the very receipt, stream, manifest, or
index it depends on without pretending to sandbox child effects.
Exact stdin/stdout/stderr streams use SHA-256-addressed storage. Familiar run-local files remain available, while identical stream content can share one read-only hard-linked object. Unsupported hard links degrade transparently to readable run-local evidence and report incomplete deduplication rather than changing the child result.
Operators use the managed interfaces instead of editing internal storage:
uv run ashctl storage status
uv run ashctl storage verify --max-issues 100
uv run ashctl close RUN_SUFFIX
uv run ashctl pin RUN_SUFFIX
uv run ashctl unpin RUN_SUFFIX
uv run ashctl gc --max-runs 1000 --max-bytes 5368709120
uv run ashctl recover --dry-runVerification re-hashes objects and run streams and checks receipt references and link identity. Garbage collection removes unreferenced objects. Recovery never pretends abandoned output is complete: it reconciles dead-owner runs into an explicit interrupted receipt with degraded-evidence metadata.
close is explicit cold storage for finalized evidence.
It verifies independent standard gzip payloads before releasing plain
hard links and leaves the receipt plus closure manifest readable. Every
managed recall/search/watch/export path transparently decodes those
files, while ordinary gzip -dc and zcat remain
available outside the project.
Artifact placement is also centralized. When the checked-in launchers
are used from another project, Agent Shell performs one bounded ancestor
walk. An explicit .agent-shell-root wins, followed by
strong VCS and project markers such as .git, Python/Node
manifests and lockfiles, dependency/build markers, and conservative
README/src fallbacks. There is no recursive scan, manifest
parse, Git invocation, or package-manager subprocess. Nested calls
converge on one target-owned .agent-shell instead of
polluting every working directory.
How Agent Shell is built
Agent Shell is a Python 3.14+ package built around a few strict
ownership rules. Pydantic models define and validate versioned requests,
receipts, queries, batches, announcements, and watch results. Process
and host observations use psutil; operational logging uses
Loguru. The important design choice is not a particular dependency, but
that each behavior has one owner.
A normal run follows a compact lifecycle:
- Validate a typed run specification and resolve the artifact root.
- Allocate a unique run directory grouped by sanitized executable name and an invocation hash; literal arguments never become path components.
- Start the child with direct argv and capture selected stdin plus stdout and stderr as exact bytes.
- Serialize stream and lifecycle observations into one elapsed-time event journal while gathering qualified resource samples.
- Finalize hashes, exit state, timing, and optional evidence into an immutable receipt, then atomically publish the current-run anchor.
- Apply content reuse and protected LRU retention.
- Extract one formatter-neutral completion model and render the requested human or JSON representation.
Advanced operations compose these same services. inspect
is one run followed by one native query. compose compiles
to the canonical batch model. Batch execution owns sequencing and
materialized dataflow. watch incrementally reads the
existing event and stream evidence. Recall helpers share one query
engine for selection, filtering, projection, bounds, cursors, and
rendering.
There is no second executor hiding behind the advanced commands, and no second interpretation of what a run means.
Capturing output, sampling resources, hashing streams, and publishing receipts add overhead compared with running a child directly. Diagnostics and indexes are built only when requested, and prepared checkout launchers avoid repeated uv startup. The benchmark commands measure these costs on the machine where the tool will run.
A practical operating playbook
For the highest productivity and clearest evidence, use these rules:
- Start with plain
ash. If the receipt or quickview answers the question, stop there. - Use
inspectwhen the desired select/filter/project operation is known before execution. It avoids a run-ID round trip. - Use bounded native queries instead of copying complete artifacts into an agent context. Export exact bytes only when another tool genuinely needs them.
- Use
composefor a few ad hoc stages:::into::for complete finite artifacts and::pipe::for concurrent streaming/early close. Use a checked-inBatchSpecfor reusable, branching, or reviewable automation. - Use full run and batch IDs in persisted automation. The final eight hex characters are an interactive convenience accepted only when unique.
- Treat an active watch result as provisional. Replay after finalization when a durable decision requires receipt-verified evidence.
- Pin evidence referenced by durable external state, then unpin it when that reference expires.
- Close finalized cold evidence when its plain payloads no longer need hot access; managed logical commands stay unchanged.
- Use JSON plus the published schemas across process boundaries; do not parse the human completion.
- Keep secrets out of argv and persisted environment selections.
- Treat telemetry and filesystem data as observations, never as containment or causal proof.
Try it from one checkout
Agent Shell requires Python 3.14+ and uv. Clone the repository and add its launchers to PATH:
git clone https://github.com/mattsta/agent-shell /opt/agent-shell
export PATH="/opt/agent-shell/bin:$PATH"
cd /workspace/another-project/src/deep/package
ash uv run pytest -q
ashctl inspect filter merged 'failed|passed' -- uv run pytest -qThe launcher resolves its own checkout even through a symlink, prepares its locked uv environment on first use or when lock inputs change, and then executes the cached entrypoint directly. It preserves the target cwd, argv, stdin, and environment. Artifacts belong to the target project, not the Agent Shell checkout.
For development inside the checkout:
uv sync --all-groups
uv run ash ls -latrh
uv run ashctl --helpThe larger idea
Traditional command execution treats text on a terminal as the product and everything else as an optional afterthought. Agent Shell treats execution as the creation of evidence: exact bytes, observed chronology, timing, resource context, exit state, integrity, and a durable identity.
The terminal response then becomes what an agent or operator actually needs at that moment: a small receipt, a bounded answer, or an extracted value.
That is a modest change in default behavior. It produces a much larger change in how reliably humans, agents, and automation can understand the same command.