session_agent_harness job type writes under
the profile’s own jobs/. For the generic layout (identity.yaml, the shared credentials file,
instance.yaml, config.yaml, logs/{instance_id}/worker.jsonl) see
Filesystem Layout.
Tree
cwd = workspace.work_folder; the worker keeps no separate agent-session
directory on disk. Session resume is SDK-managed and keyed by agent_session_id (recorded in
thread.yaml).
job_id is deterministic: session_agent_harness-{session_id}, always keyed on the resolved session
id — even when config.yaml names the session by alias (see Configuration). This
guarantees stable paths across restarts, reinstalls, and config edits — and namespaces this job type away
from any future ones.
Nested thread state for path aliases
A thread’s alias can itself contain/, which is what an application with a path-structured namespace
hands off, e.g. projects/<pid>/items/<iid>/runs/<n>/thread. Each /-separated segment becomes one
nested directory, so that alias lands at:
my-feature-x (no /) lands exactly where it always has:
threads/my-feature-x/thread.yaml. Both shapes coexist under the same threads/ root without collision —
directory and file names are just alias segments, so the state for alias a and the directory for alias
a/b don’t conflict (threads/a/thread.yaml vs. threads/a/b/thread.yaml).
File-by-file
section.yaml — per-section runtime state
Persisted by the worker. Recovery on next start re-reads this.
session.session_id here is recorded exactly as configured — a uuid, or an alias if that’s how
config.yaml names it. It is not necessarily the resolved id used to derive job_id (the directory
name one level up): if you configure an alias, this field keeps showing that alias while the enclosing
directory is always named after the resolved uuid.
not_mine.yaml — classification cache
An attach-time optimisation: the set of aliases under this section’s watched prefix that were downloaded
once and found not to be the worker’s own work (a graph, a record, a published artifact — anything
that isn’t a thread-typed object carrying an agent block or an instance.state). A later attach skips
the download for every alias already in this set.
type each session_object_modified event carries, so a stale entry here can delay picking up a
newly-relevant alias by at most one attach, and can never suppress it permanently. Every artifact alias the
worker publishes itself (see Job Session Object → Artifacts)
is added here as it’s uploaded — artifacts are the dominant volume in a busy namespace, so this is what
keeps a cold attach to a long-lived instance bounded. See
Concepts → Namespace scoping & discovery for the full discovery model this cache
supports.
logs/section.log
Section-scoped JSON-line log: poll cadence, event dispatch, thread discovery.
The local outbox — inside work_folder, not under jobs/
When a thread’s envelope carries an artifacts block, the worker creates
<work_folder>/<artifacts.outbox> (with parent directories) the moment that thread activates — so the
folder exists before the agent’s first turn looks for it. Unlike everything else on this page, the outbox
lives inside the repository checkout (work_folder), not under ~/.blobhub-worker/profiles/{profile}/jobs/. That is a
constraint, not a layout preference: under approval or guarded, Codex runs its shell and file-edit
tools inside a workspace-write OS sandbox scoped to work_folder, so a write outside it doesn’t happen
silently — it escalates into a fileChange approval request first, the same request an out-of-workspace
write anywhere else would trigger. An unattended publish step can’t depend on someone answering that
request every turn, so the worker sidesteps the question entirely by keeping the outbox inside the
work_folder the sandbox already grants.
The worker makes the folder it creates self-ignoring: it writes a .gitignore containing a single *
into the outbox root. work_folder is typically a git checkout, so without this, scratch output would show
up in git status and could be swept into an agent’s own git add -A. A single * ignores the directory’s
entire contents — including the .gitignore file itself — needs no cooperation from the repository’s own
ignore rules, and works no matter what artifacts.outbox turns out to be. Writing it is idempotent and
confined to the outbox root the worker itself created; the worker never deletes, moves, or rewrites a file
already there.
See Job Session Object → Artifacts for the publish contract
and what happens to the files once they’re in this folder.
The local inbox — also inside work_folder, not under jobs/
The mirror image of the outbox above: the worker resolves an inbox directory for every thread at
activation (three-tier — thread, then worker config, then the codebase default .blobhub/inbox; see
Job Session Object → Attachments and
Configuration → attachments) and creates it before that thread’s
first turn runs. Incoming file attachments land at:
ensure_inbox is the same containment-checking, self-.gitignore-writing routine artifacts uses
for the outbox (a single * written into the inbox root), for the same reasons: the inbox lives inside a
git checkout the agent can git add -A, and an escaping path is refused rather than resolved.
Because both folders can live inside the same work_folder, the outbox walk that
artifacts publishes from excludes the inbox
subtree — files the worker itself downloaded for the agent to read are not the agent’s own output, and
republishing them back as artifacts would be a report about the worker’s own machinery, not the agent’s
work.
The on-disk inbox is the durable copy, not necessarily how the model first sees a file. When the item’s
block order puts an image between two runs of text, the worker also hands that image to the harness
natively, in place — Claude Code and Codex receive it at its composed position; Antigravity’s machine
input takes text only, so there every file goes by path — so the path below
is the fallback and the record, reached for a non-image file or whenever native delivery isn’t taken. See
Job Session Object → Attachments.
<safe-name> is never the attachment’s name verbatim — a client-supplied name arrives over the wire
with no length limit and no guarantee it’s even a valid path segment, so it’s sanitized before it ever
touches disk:
- Backslashes normalize to forward slashes, then only the last non-empty path segment survives — a
name like
../../etc/passwdorsub/dir/file.txtcontributes onlypasswdorfile.txt. - Every character outside
[A-Za-z0-9._@-]becomes_, and any leading dots are stripped (no hidden-file, no..). - An empty result (a name that sanitized down to nothing) falls back to
file-{ordinal}, the file’s 1-indexed position among that item’s attachments. - The result is capped at 128 characters, preserving a recognizable extension: an extension of up to 16
characters is kept whole and the stem in front of it is trimmed to fit; anything longer than that, or a
name with no
.at all, is truncated flat instead.
The managed hook config — antigravity only, and only while threads run
The third thing the worker writes inside a work_folder, and the only one that is not the worker’s own
scratch space. An antigravity thread running under permissions: guarded or approval is gated by a
PreToolUse hook (see
Job Session Object → Antigravity: the gate is a hook in another process),
and the Antigravity CLI discovers hooks from the workspace root and nowhere else. So the config has to
live in the repository you pointed the thread at:
work_folder, so the worker refcounts the file per resolved path — written when the first one
starts, restored when the last one leaves, not written and removed per turn.
It never clobbers a file you already have. .agents/ is a workspace convention you may already use for
rules, skills, or agents of your own. An existing hooks.json is read, the worker’s one named key is
merged into it, and on release the file is restored byte-for-byte to what it was — or deleted, if it
was the worker’s alone. The .agents/ directory itself is removed only when the worker created it and
nothing else has since moved in.
Two things to expect while it is there:
- It shows up in
git status.work_folderis usually a checkout, and unlike the outbox and inbox this file is not self-ignored — an ignore rule would be a change to your repository that outlives the thread. Add.agents/hooks.jsonto your own ignore rules if the noise matters. - The hook command carries its own fallback, and that is load-bearing rather than defensive dressing.
Every failure mode of an Antigravity hook fails closed: a command that cannot be run blocks the tool
call. A
hooks.jsonleft behind by a worker that crashed or was uninstalled would therefore break every tool call in that folder — including in your own, unrelated Antigravity sessions. The configured command iscommand -v blobhub-worker … && blobhub-worker agy-gate || echo '{"decision":"allow"}', which degrades that specific case to inert instead of bricking the directory. A stale file is still worth deleting; it just is not an emergency.
threads/{alias}/thread.yaml — per-thread state
The worker’s local, authoritative-for-recovery view of a thread, plus the inbound/outbound item
cursors. This local layout differs from the wire envelope: it keeps workspace.agent_type and
agent.state, and the agent_session_id resume pointer and error block are local-only — they
never appear on the wire. The mapping is documented on
Job Session Object.
threads/{alias}/logs/thread.log
Captures the full untruncated agent stdio + tool I/O. When the worker truncates a thread item
to fit the 60 KiB budget, the local log retains the full payload — see
Thread Items.
Why both server-side metadata and thread.yaml?
- The server-side envelope is authoritative for handoff. Anyone with API access (web UI, SDK, another tool) reads it to see the agent state machine.
thread.yamlis authoritative for local recovery. The worker writes it before any externally observable side-effect, so a crash-restart finds a truthful local record.
See also
- Filesystem Layout — generic top-level files.
- Job Session Object — wire vs local field layout, the
artifactsandpolicyblocks. - Concepts — the list+watch discovery model this page’s
not_mine.yamlsupports. - Configuration
- Recovery

