agent-shell: your output, your storage

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 grep reports 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.
  • and a more toxic approach is where models decide to use live public URLs for reference material then curl | grep except, again, models try to “guess grep filters”, fail, then re-run the curl a 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 merged

So, 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.txt

The 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 -q

The 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 merged

That 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.json

The 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.bin and stderr.bin contain the exact captured bytes for each stream.
  • events.jsonl records 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_SUFFIX

The 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 --workspace

This 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 -q

The 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_ms

Sources 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.md

When 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 json

The 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 json

The 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 schemas

Human 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-run

Verification 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:

  1. Validate a typed run specification and resolve the artifact root.
  2. Allocate a unique run directory grouped by sanitized executable name and an invocation hash; literal arguments never become path components.
  3. Start the child with direct argv and capture selected stdin plus stdout and stderr as exact bytes.
  4. Serialize stream and lifecycle observations into one elapsed-time event journal while gathering qualified resource samples.
  5. Finalize hashes, exit state, timing, and optional evidence into an immutable receipt, then atomically publish the current-run anchor.
  6. Apply content reuse and protected LRU retention.
  7. 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:

  1. Start with plain ash. If the receipt or quickview answers the question, stop there.
  2. Use inspect when the desired select/filter/project operation is known before execution. It avoids a run-ID round trip.
  3. Use bounded native queries instead of copying complete artifacts into an agent context. Export exact bytes only when another tool genuinely needs them.
  4. Use compose for a few ad hoc stages: ::into:: for complete finite artifacts and ::pipe:: for concurrent streaming/early close. Use a checked-in BatchSpec for reusable, branching, or reviewable automation.
  5. Use full run and batch IDs in persisted automation. The final eight hex characters are an interactive convenience accepted only when unique.
  6. Treat an active watch result as provisional. Replay after finalization when a durable decision requires receipt-verified evidence.
  7. Pin evidence referenced by durable external state, then unpin it when that reference expires.
  8. Close finalized cold evidence when its plain payloads no longer need hot access; managed logical commands stay unchanged.
  9. Use JSON plus the published schemas across process boundaries; do not parse the human completion.
  10. Keep secrets out of argv and persisted environment selections.
  11. 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 -q

The 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 --help

The 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.