The work directory
~/.mecha/work/<producer>/ is where a run's generated output goes, and it is
also the run's workspace — the directory the path jail is rooted at.
A producer is whatever made the output: a trigger's name, a task's id (for
mecha tasks work and a web chat on a task), or web or slack, which keep
one subdirectory per session or thread beneath them (and mecha work path
names any other you choose). The directory is stable across runs of the same producer, which
is most of the point. Yesterday's briefing is an ordinary file in today's run,
readable with fs_read like anything else, rather than something that has to
be fetched back from somewhere outside the jail.
mecha work # what each producer has generated
mecha work path briefing # print one producer's directory, creating it
mecha work clean # keep the newest N per producer
mecha work clean --dry-run # say what would go, remove nothing
mecha work clean --producer briefing --keep 3
Two directories that mean opposite things
~/.mecha/work/<producer>/ generated · mutable · disposable · cleanable
~/.mecha/bundles/<id>/<ver>/ published · immutable · versioned · never deleted
Work is scratch that a retention policy sweeps. A published bundle is a durable, versioned URL that nothing here ever removes. Keeping them apart is what lets the sweep be aggressive.
One change, four things closed
The work directory is small, and it exists because a single change closed four separate problems — which is usually the sign that the shape is right.
| It fixes | How |
|---|---|
| The jail default | An unattended run's workspace now holds nothing sensitive. |
| Cross-run read-back | The directory is stable, so yesterday's output is today's input. |
| A durable artifact | An unattended run has somewhere to leave something you can open later. |
notify | It has a designated place to write instead of inventing one. |
The jail one is the load-bearing fix. An unattended run holding filesystem
tools needs a jail that holds nothing sensitive, and the obvious alternative —
whatever directory the scheduler was started in, usually $HOME — contains
~/.mecha/: the mail OAuth tokens, every session transcript, the learning
store.
A workspace inside the mecha home is fine, and is now the default. What
setup refuses is a workspace the mecha home sits under — which is what
mecha chat in $HOME was doing. See the security
model.
And notify runs in the same directory, so a trigger that wants to keep its
answer writes it there — a relative path — where the next run can read it back,
instead of inventing a directory outside every path jail.
Where a trigger's workspace comes from
mecha trigger add writes the workspace down rather than leaving it
implicit, and the runner resolves the same default when the field is unset — so
a trigger authored before this behaviour existed is fixed by upgrading, not by
remembering to edit it. mecha trigger show prints the resolved default too:
"where is this jailed" must never be answered by an omitted line.
mecha trigger show briefing
# ...
# workspace ~/.mecha/work/briefing (default)
Retention is a policy, not an intention
Anything without one becomes a pile nobody opens.
[work]
keep = 10 # entries per producer that survive `mecha work clean`
mecha work clean keeps the newest keep entries per producer and says
exactly what it removed. The nightly maintenance run calls it. Three rules:
- Entries are counted, not files. A rendered bundle is a directory, and it counts as one entry.
- The producer directory itself is never removed. An empty directory is a directory, not an absence, and deleting it would only make tomorrow's run recreate it.
- It never removes anything a published bundle names as a source. "Regenerate last week's report" must not silently lose its input. Entries that survive for this reason are reported, not silently skipped — an unexplained survivor reads as a bug in the sweep.
The default of 10 holds about a week and a half of a daily producer, so both
"what did yesterday's run say" and "what changed since Monday" are still on
disk. It is a placeholder in the honest sense: it wants a week of real output
to tune, and [work] keep is where that tuning goes.
How a bundle protects its sources
The contract is one field of data, not a shared type. A mirrored version
directory may carry a bundle.json with a sources array:
{
"sources": ["/home/you/.mecha/work/briefing/2026-08-05"]
}
Anything else in that file is the publisher's business. mecha-factory-publish
writes it: what a bundle was rendered from travels into bundle.json as
sources. A machine with no mirror at all — nothing ever published — protects
nothing, and that is correct rather than a stub.
Naming
A producer name has to be a single safe path segment. mecha work path and
trigger add both validate it, so a trigger called ../../etc is an error at
creation rather than a traversal at run time.
$MECHA_HOME relocates the whole tree. It exists for tests and for running two
mechas side by side; nothing in a normal install sets it.
Where to go next
- Triggers — the producer that most needs a durable place to write.
- Publishing — what turns a work directory into a URL.
- Security model — the path jail this is rooted at.