ADR-0003: Runtime home/ directory as the canonical container HOME (2026-05-14)

Status: Accepted. Supersedes: Implicit layout from runtime/agents/<name>/ + runtime/workspaces/<name>/ (both unused after this ADR). Related: 0001-isolation-hardening.md — D5 introduced --home /home/agent but didn’t relocate the host-side directory the container HOME points at.

Problem

After D5 (docs/adr/0001-isolation-hardening.md), sac auto-injects --home /home/agent so $HOME is operator-independent inside the container. But _dot_claude.py::deploy_dot_claude(config, workdir) still materialises dot_claude/* into <host-workdir>/.claude/, and the host workdir is bind-mounted at /work — not at /home/agent.

Consequence:

  • /work/.claude/CLAUDE.md is found by Claude’s auto-walk-from-cwd (still works for CLAUDE.md).

  • /home/agent/.claude/skills/, /home/agent/.claude/.mcp.json, /home/agent/.claude/hooks/, /home/agent/.claude/commands/ — empty. Claude SDK’s $HOME/.claude/-rooted discovery never sees the operator’s dot_claude/ contents.

The runtime layout also carries vestigial empty directories (runtime/agents/<name>/, runtime/workspaces/<name>/) from earlier designs that were never wired through, which makes the “where does sac actually put things?” question ambiguous.

Decision

D6. Per-agent runtime directory = the container’s $HOME source.

For each agent, sac maintains:

~/.scitex/agent-container/runtime/<name>/
├── home/                  ← bind-mounted at /home/agent (canonical $HOME)
│   ├── .claude/           ← dot_claude materialised here
│   │   ├── CLAUDE.md
│   │   ├── .mcp.json
│   │   ├── skills/
│   │   ├── hooks/
│   │   └── commands/
│   └── (anything the agent writes to $HOME — proj/, .cache/, ...)
├── overlay.img            ← per-agent writable layer (was: runtime/overlays/<name>.img)
├── pid
├── apptainer_pid
├── heartbeat.json
├── session_id
├── session.jsonl
├── quota.json
└── stdout.log

The bind:

~/.scitex/agent-container/runtime/<name>/home  →  /home/agent  (rw)

So inside the container:

  • $HOME == /home/agent (D5 invariant).

  • $HOME/.claude/skills/ resolves to the materialised tree — Claude SDK’s discovery works without further config.

  • Anything the agent writes under $HOME persists on the host at runtime/<name>/home/, browsable from the operator side without entering the container.

D7. dot_claude materialises into runtime/<name>/home/.claude/.

_dot_claude.py::deploy_dot_claude is rewritten to:

  1. Resolve runtime/<name>/home/.claude/ as the target.

  2. Materialise dot_claude/CLAUDE.md home/CLAUDE.md, dot_claude/.mcp.json home/.mcp.json, dot_claude/.env home/.env, dot_claude/state.md home/state.md.

  3. Mirror dot_claude/{commands,skills,hooks,agents,settings,...}/home/.claude/<same>/.

The workdir /work (which still mounts the operator’s host workdir) no longer receives a .claude/ materialisation — it stays a pure project-content mount.

D8. Overlay path consolidation.

apptainer.overlay default resolves to runtime/<name>/overlay.img (previously runtime/overlays/<name>.img). Per-agent state lives under runtime/<name>/ exclusively; the old runtime/overlays/ location is left in place for backward compatibility but new agents write to the new location.

D9. Vestigial directories deprecated.

runtime/agents/<name>/ and runtime/workspaces/<name>/ are no longer used by sac. They remain in the filesystem for now (no destructive cleanup); a future sac runtime cleanup command can remove them.

Migration

  • Existing agents — on next sac agents start:

    • runtime/<name>/home/ is auto-created if missing.

    • dot_claude re-materialises into the new location.

    • Existing runtime/<name>/{pid,session.jsonl,…} files keep their current location (this ADR doesn’t move them).

    • Existing runtime/overlays/<name>.img continues to work if apptainer.overlay points at it explicitly; new agents get runtime/<name>/overlay.img by default.

  • No data loss — the refactor adds a directory; doesn’t delete or move existing data.

Rationale

Why name the directory home/, not workspace/?

The semantic name (home/) directly matches what it becomes inside the container (/home/agent). Operators reading runtime/scitex-stats-auditor/home/.claude/skills/ instantly know “this is what the agent sees as ~/.claude/skills/ inside the container.” workspace/ requires a glossary lookup.

The bind line is self-documenting:

runtime/<name>/home  →  /home/agent
       (host home)      (container home)

This matches Kubernetes’ philosophy of naming things by their container-side semantic (volumeMounts.mountPath), not the host-side implementation detail.

Why home/ inside runtime/<name>/, not as a sibling?

Per-agent state should be browsable as one directory tree. Operators running du -sh runtime/<name>/ should see the total footprint of that agent (overlay + home + logs + session). Splitting across parallel trees (runtime/<name>/, runtime/overlays/<name>.img, runtime/workspaces/<name>/) makes the “what does this agent use?” question harder than it needs to be.

Relationship to “strict vs relaxed” deployment modes

This ADR is layout-only. The strict-vs-relaxed mode distinction (Clew reproducibility runs vs everyday development) layers on top:

  • strict: runtime/<name>/home bound :ro, project sources :ro, no shared skills bind. Promises the verification chain that $HOME was immutable during the run.

  • relaxed: runtime/<name>/home bound :rw, project sources potentially :rw, host-side ~/.claude/skills/ optionally bound in for cross-agent skill sharing.

A separate ADR (0004) will formalise the mode field. This ADR just gives both modes a sensible directory to bind.

Implementation

Layer

Where

Status

Auto-create runtime/<name>/home/ on start

_apptainer_runtime.py::start

⏳ this PR

Auto-prepend bind home:/home/agent:rw

_apptainer_runtime.py::build_run_argv

⏳ this PR

Materialise dot_claude/ into runtime/<name>/home/.claude/

_dot_claude.py::deploy_dot_claude

⏳ this PR

Default apptainer.overlayruntime/<name>/overlay.img

_apptainer_runtime.py resolution

⏳ this PR

ADR + docs/isolation.md cross-ref

docs

✅ this commit

Consequences

Positive.

  • Claude SDK’s $HOME/.claude/-rooted discovery (skills, hooks, .mcp.json) works without operator intervention.

  • Per-agent state lives in one tree (runtime/<name>/) — backup, cleanup, and inspection commands stay simple.

  • Operator-side debugging: ls runtime/<name>/home/ shows what the agent sees as its $HOME — no container shell needed.

  • Layout name aligns with container-side semantic — fewer glossary jumps in docs.

Negative / tradeoffs.

  • One extra bind in every container’s argv. Negligible startup cost.

  • dot_claude/ materialised twice during the transition window: once into runtime/<name>/home/.claude/ (new) and old <workdir>/.claude/ may still exist on disk from prior runs. Cleanup deferred; not a correctness issue.

References