Skip to main content

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, or chat, or a session id. 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 fixesHow
The jail defaultAn unattended run's workspace now holds nothing sensitive.
Cross-run read-backThe directory is stable, so yesterday's output is today's input.
A durable artifactAn unattended run has somewhere to leave something you can open later.
notifyIt has a designated place to write instead of inventing one.

The jail one is the load-bearing fix. A trigger with no explicit workspace used to fall through to std::env::current_dir(), and the shipped systemd unit sets WorkingDirectory=%h. So an unattended run holding filesystem tools was jailed to $HOME — which contains ~/.mecha/: the mail OAuth tokens, every session transcript, the learning store. The shipped morning trigger escaped only by accident of its mail__* allowlist.

:::note Note the direction of the check 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 used to end with mkdir -p ~/.mecha/briefings && cat > …: a shell redirect into a directory it created on the way past, outside every path jail, so nothing could ever read it back. That existed only because there was no designated place to write.

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. A mirror that does not exist yet — which is every install until mecha-factory-publish is wired — 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.