-
Notifications
You must be signed in to change notification settings - Fork 0
container filesystem
This page is a consolidated reference for every host-path → container-path mapping that
harnessed creates when it launches a stack. Use it to understand where files land inside
the container, diagnose mount problems, and author persist: / init: entries correctly.
Two invariants govern all mounts:
-
Profile is assembled ahead of time. Nothing in
$XDG_DATA_HOME/harnessed/profiles/<stack>/is resolved, templated, or patched at container start — whatharnessed buildwrote is what the container sees, byte-for-byte. -
No secrets baked into images, and credentials never resolved by
harnessed build. Every secret surface (.credentials.json, SSH agent socket, opt-in git credentials) reaches the container only at launch time, as a bind-mount or env-file, and only when the host-side file exists.harnessed buildnever invokes varlock and never touches any credential at all — see Secrets guide for the deliberate separation between build and launch.
| Value | Detail |
|---|---|
| Container user |
harnessed (renamed from Ubuntu's default ubuntu) |
$HOME |
/home/harnessed |
CONTAINER_HOME constant |
paths.CONTAINER_HOME = Path("/home/harnessed") |
The base image (
catalog/base/Dockerfile.harnessed-base) setsENV HOME=/home/harnessed. Every in-container path that starts with$HOMEor~resolves there.
Assembled by harnessed build into $XDG_DATA_HOME/harnessed/profiles/<stack>/.
All mounts below are read-only (ro). Only harnesses that actually consume each file
receive the mount (claude/omp/opencode for the .claude/ tree; claude-only for .mcp.json).
| Host path | Container path | Notes |
|---|---|---|
profiles/<stack>/.mcp.json |
$HOME/.mcp.json |
claude only; passed to --mcp-config
|
profiles/<stack>/settings.json |
$HOME/.claude/settings.json |
claude, omp, opencode |
profiles/<stack>/.claude/skills/ |
$HOME/.claude/skills/ |
claude, omp, opencode |
profiles/<stack>/.claude/commands/ |
$HOME/.claude/commands/ |
claude, omp, opencode |
profiles/<stack>/.claude/agents/ |
$HOME/.claude/agents/ |
claude, omp, opencode |
profiles/<stack>/.claude/hooks/ |
$HOME/.claude/hooks/ |
claude, omp, opencode |
profiles/<stack>/.claude/rules/ |
$HOME/.claude/rules/ |
claude, omp, opencode |
profiles/<stack>/hatago.config.json |
$HOME/hatago.config.json |
all harnesses |
where profiles/ expands to $XDG_DATA_HOME/harnessed/profiles/ (defaults to
~/.local/share/harnessed/profiles/).
The project directory (or a parent via --mount-folder) is mounted path-mirrored: the
same absolute path on the host appears at the same absolute path inside the container. This
means the agent sees host paths directly — no path translation needed.
| Host path | Container path | Mode |
|---|---|---|
<mount_path> |
<mount_path> (same) |
rw |
The agent's working directory (cwd) is set to <project_path> (or --agent-start-folder
if specified). With --mount-folder, <mount_path> is a parent of <project_path> — the
whole subtree is visible but the agent starts inside the project.
These dirs are sourced from the host $HOME/.claude/ and mounted read-write so Claude's
session history, file-tracking, and todos persist across container restarts. Dirs are
created on the host automatically if they don't exist.
| Host path | Container path | Mode |
|---|---|---|
~/.claude/projects/ |
$HOME/.claude/projects/ |
rw |
~/.claude/file-history/ |
$HOME/.claude/file-history/ |
rw |
~/.claude/tasks/ |
$HOME/.claude/tasks/ |
rw |
~/.claude/session-env/ |
$HOME/.claude/session-env/ |
rw |
~/.claude/todos/ |
$HOME/.claude/todos/ |
rw |
| Host path | Container path | Mode | Notes |
|---|---|---|---|
~/.claude/.credentials.json |
$HOME/.claude/.credentials.json |
ro |
Only mounted when file exists |
$XDG_STATE_HOME/harnessed/<instance>/claude.json |
$HOME/.claude.json |
rw |
Per-instance onboarding stub; seeded from host ~/.claude.json (account identity only, never the OAuth token) |
<instance> is harnessed-<stack>-<sha1[:8] of project path>.
$XDG_STATE_HOME defaults to ~/.local/state.
The stub at .claude.json marks hasCompletedOnboarding: true so Claude Code skips its
first-run login screen. The real authentication is ~/.claude/.credentials.json (OAuth
token) — the stub never contains token material.
| Host path | Container path | Mode | Notes |
|---|---|---|---|
~/.omp/agent/ |
$HOME/.omp/agent/ |
rw |
omp harness only; full host-state sharing |
Forwarded automatically whenever the host agent socket is live — no forward_git_credentials
needed. Safe as a default because the agent exposes no key material and gates every sign/auth behind
a host-side 1Password approval or YubiKey touch, and the git config is a public signing-key
reference, not a secret. All read-only unless noted; only mounted when the host file/socket exists.
| Host path | Container path | Mode | Notes |
|---|---|---|---|
~/.1password/agent.sock (Linux) or macOS equivalent |
$HOME/.1password/agent.sock |
socket | 1Password SSH agent; sets $SSH_AUTH_SOCK
|
gpg-agent SSH socket (gpgconf --list-dirs agent-ssh-socket) |
$HOME/.gnupg-sockets/S.gpg-agent.ssh |
socket | YubiKey fallback; sets $SSH_AUTH_SOCK when 1Password absent |
~/.config/git/ (or ~/.gitconfig) |
$HOME/.config/git/ (or $HOME/.gitconfig) |
ro |
Carries user.signingkey, gpg.format=ssh, etc. |
The secret-bearing surface — only mounted when a stack sets forward_git_credentials: true, and
only when the corresponding host file/dir/device exists. All read-only unless noted.
| Host path | Container path | Mode | Notes |
|---|---|---|---|
~/.config/gh/hosts.yml |
$HOME/.config/gh/hosts.yml |
ro |
gh oauth token — so gh authenticates as you |
~/.gnupg/pubring.kbx, trustdb.gpg, gpg.conf, gpg-agent.conf, sshcontrol
|
$HOME/.gnupg/<name> |
ro |
Non-secret GPG surface only; private-keys-v1.d/ is explicitly excluded |
~/.ssh/config, known_hosts, *.pub
|
$HOME/.ssh/<name> |
ro |
Non-secret SSH surface |
~/.ssh/<name> where <name> is in stack.yaml ssh_keys:
|
$HOME/.ssh/<name> |
ro |
Opt-in private keys; honored only from user overlay catalog |
| YubiKey USB device |
--device passthrough |
— | Linux only; when YubiKey is attached |
Recipe persist: entries create bind-mounts that survive --fresh restarts. The scope ×
location axes determine how the host path is constructed. For the full authoring schema
(field names, VCS control, valid combinations), see Recipe authoring — persist:.
| Host path template | Container path | Mode |
|---|---|---|
persist/<recipe>/<sha1[:8] of realpath(project_path)>/<name>/ |
$HOME/<name>/ |
rw |
persist/ expands to $XDG_DATA_HOME/harnessed/persist/ (defaults to
~/.local/share/harnessed/persist/). The key is the SHA-1[:8] of the resolved launch
path — two worktrees of the same git checkout get separate dirs.
| Host path template | Container path | Mode |
|---|---|---|
persist/<recipe>/<sha1[:8] of git-common-dir>/<name>/ |
$HOME/<name>/ |
rw |
The key is derived from git rev-parse --path-format=absolute --git-common-dir — the same
path for every worktree of a given checkout, so all worktrees share one host dir. Falls back
to the workspace hash (with a warning) when project_path is not inside a git repository.
Does not survive an independent git clone of the same repo (git-common-dir changes).
| Host path | Container path | Mode |
|---|---|---|
<path> (the real host path, allowlisted) |
<path> (same, path-mirrored) |
rw |
The path is mounted path-preserving (host path == container path) so the tool finds its
data where it expects. Gated by $XDG_CONFIG_HOME/harnessed/persist-allowlist (default
~/.config/harnessed/persist-allowlist). Several sensitive dirs (~/.ssh, ~/.aws,
~/.gnupg, ~/.config/harnessed, $HOME itself) are hard-denied regardless of the
allowlist.
Path-preserving ≠ $HOME-relative. The pod's $HOME is /home/harnessed, so a host
dotdir mounted this way lands at /home/you/.foo, not ~/.foo — a tool that looks under
$HOME will not find it. The launcher exports HOST_HOME into the attach shell for exactly
this case; point the tool at the mirrored path from the recipe's init:. The pulumi recipe
is the reference implementation (see the Pulumi guide):
persist:
- scope: global
path: ~/.pulumi # the real host dir: credentials.json + the plugin cache
init:
run: export PULUMI_HOME="$HOST_HOME/.pulumi"No additional bind-mount: the entry lives inside the path-mirrored workspace mount, which
is already rw. vcs: ignored causes harnessed to append the entry name to the
project's .gitignore idempotently. vcs: tracked leaves .gitignore untouched.
init: has no host-side marker (the declarative marker was removed — "Model A"). The run
command is sourced inline in the attach shell before the harness starts, on every attach, so it
must self-gate cheaply (e.g. bd list >/dev/null 2>&1 || bd init). There are no per-recipe marker
files and no transient podman run --rm. See
recipe authoring — One-time init.
| Host path | Container path | Mode | Notes |
|---|---|---|---|
catalog/base/egress-firewall.sh |
/usr/local/sbin/egress-firewall |
ro |
Applied by _apply_firewall after the container starts; not configurable per recipe |
-
Recipe authoring —
persist:andinit:schema reference (field names, valid scope/location combos,vcs:, worked examples) - harnessed-design.md — the design rationale behind the mount model (profile-ahead-of-time, path mirroring, no-host-defaults invariant, secrets threat model)
-
research/home-folder-claude-requirements.md —
why the specific
.claude/subpaths are forwarded from the host (and which are intentionally excluded) -
Stacks —
state.session_stateinstack.yamlis a different, stack-level mechanism (where Claude session history volumes land:hostvsvolume) and is currently unimplemented in the launcher. Do not conflate it with recipepersist:entries, which are fully implemented and described on this page. - Secrets (varlock + 1Password) — how secrets reach the container at launch time without being baked into the image or the profile
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)