Native session shadow-home never links ~/.claude.json, wiping trust/onboarding state every restart #878

Closed
opened 2026-08-04 22:06:51 +00:00 by coilyco-ops · 1 comment
Member

Symptom

Every native session (aos native) requires re-trusting project folders and shows fresh-install-like behavior (onboarding-ish resets), even though the host account has been onboarded for months.

Root cause

The native launcher sets CLAUDE_CONFIG_DIR to the shadowed .claude directory inside the ephemeral per-session home ($TMPDIR/aos/native/<session-id>/home/.claude). Claude Code 2.1.221 stores its primary state file — hasTrustDialogAccepted per project, hasCompletedOnboarding, oauth cache, numStartups, projects map — at $CLAUDE_CONFIG_DIR/.claude.json.

The shadow-home stager in aos-cli/native_shadow.go (stageNativeRoleHome / stageNativeRoleConfigDirectory, ~lines 831-900) special-cases .claude by walking whatever already exists inside the real ~/.claude/ and symlinking each entry into the shadow copy. But on the real host, the state file Claude Code actually needs lives one directory up, at ~/.claude.json — not ~/.claude/.claude.json. Nothing under real ~/.claude/ is named .claude.json, so the stager never enumerates or links it.

Result: every native session gets a brand-new, empty .claude.json created fresh inside the shadow .claude/ dir instead of a link to the real 196KB file with full history.

Evidence (captured live from a running native session)

Real host ~/.claude.json:

  • numStartups: 864
  • projects: 169 entries, each with hasTrustDialogAccepted, lastSessionId, etc.

Shadow $CLAUDE_CONFIG_DIR/.claude.json (i.e. .../home/.claude/.claude.json) at session start:

  • numStartups: 1
  • firstStartTime: the session's own boot timestamp
  • projects: 1 entry — the ephemeral session's own temp project path only

The top-level shadow ~/.claude.json symlink (home/.claude.json -> /Users/kai/.claude.json) is correctly wired but is dead weight: Claude Code never reads it once CLAUDE_CONFIG_DIR is set, because that env var redirects lookup to $CLAUDE_CONFIG_DIR/.claude.json instead.

Where CLAUDE_CONFIG_DIR gets set

Not found in the currently-checked-out aos-cli Go source (grep -rn CLAUDE_CONFIG_DIR aos-cli/ is empty). Either the installed aos binary is ahead of the checked-out source, or it's set elsewhere (shell wrapper?). Worth reconciling aos --version against repo HEAD before landing a fix, since the fix needs to touch whichever code path actually sets it.

Suggested fix

stageNativeRoleConfigDirectory needs to also link (or the caller needs to separately link) the real ~/.claude.json to $CLAUDE_CONFIG_DIR/.claude.json in the shadow tree — i.e. treat it as the same logical file regardless of which physical path CLAUDE_CONFIG_DIR puts it at, rather than only mirroring pre-existing children of ~/.claude/.

Repro

  1. Start a native session (aos native ...).
  2. echo $CLAUDE_CONFIG_DIR — note it points inside the ephemeral home.
  3. Compare python3 -c "import json; print(json.load(open('$CLAUDE_CONFIG_DIR/.claude.json'))['numStartups'])" against the real ~/.claude.json's numStartups. The shadow copy is always freshly reset to 1.
## Symptom Every native session (`aos native`) requires re-trusting project folders and shows fresh-install-like behavior (onboarding-ish resets), even though the host account has been onboarded for months. ## Root cause The native launcher sets `CLAUDE_CONFIG_DIR` to the shadowed `.claude` directory inside the ephemeral per-session home (`$TMPDIR/aos/native/<session-id>/home/.claude`). Claude Code 2.1.221 stores its primary state file — `hasTrustDialogAccepted` per project, `hasCompletedOnboarding`, oauth cache, `numStartups`, `projects` map — at `$CLAUDE_CONFIG_DIR/.claude.json`. The shadow-home stager in `aos-cli/native_shadow.go` (`stageNativeRoleHome` / `stageNativeRoleConfigDirectory`, ~lines 831-900) special-cases `.claude` by walking whatever *already exists* inside the real `~/.claude/` and symlinking each entry into the shadow copy. But on the real host, the state file Claude Code actually needs lives **one directory up**, at `~/.claude.json` — not `~/.claude/.claude.json`. Nothing under real `~/.claude/` is named `.claude.json`, so the stager never enumerates or links it. Result: every native session gets a brand-new, empty `.claude.json` created fresh inside the shadow `.claude/` dir instead of a link to the real 196KB file with full history. ## Evidence (captured live from a running native session) Real host `~/.claude.json`: - `numStartups: 864` - `projects`: 169 entries, each with `hasTrustDialogAccepted`, `lastSessionId`, etc. Shadow `$CLAUDE_CONFIG_DIR/.claude.json` (i.e. `.../home/.claude/.claude.json`) at session start: - `numStartups: 1` - `firstStartTime`: the session's own boot timestamp - `projects`: **1 entry** — the ephemeral session's own temp project path only The top-level shadow `~/.claude.json` symlink (`home/.claude.json -> /Users/kai/.claude.json`) is correctly wired but is dead weight: Claude Code never reads it once `CLAUDE_CONFIG_DIR` is set, because that env var redirects lookup to `$CLAUDE_CONFIG_DIR/.claude.json` instead. ## Where CLAUDE_CONFIG_DIR gets set Not found in the currently-checked-out `aos-cli` Go source (`grep -rn CLAUDE_CONFIG_DIR aos-cli/` is empty). Either the installed `aos` binary is ahead of the checked-out source, or it's set elsewhere (shell wrapper?). Worth reconciling `aos --version` against repo HEAD before landing a fix, since the fix needs to touch whichever code path actually sets it. ## Suggested fix `stageNativeRoleConfigDirectory` needs to also link (or the caller needs to separately link) the real `~/.claude.json` to `$CLAUDE_CONFIG_DIR/.claude.json` in the shadow tree — i.e. treat it as the same logical file regardless of which physical path `CLAUDE_CONFIG_DIR` puts it at, rather than only mirroring pre-existing children of `~/.claude/`. ## Repro 1. Start a native session (`aos native ...`). 2. `echo $CLAUDE_CONFIG_DIR` — note it points inside the ephemeral home. 3. Compare `python3 -c "import json; print(json.load(open('$CLAUDE_CONFIG_DIR/.claude.json'))['numStartups'])"` against the real `~/.claude.json`'s `numStartups`. The shadow copy is always freshly reset to `1`.
Author
Member

Fixed on main, in the shape this issue proposed. The commit referenced #956 and not this one, so it stayed open.

974c9471 ("fix(aos): project the Claude config into native session homes", 2026-08-06) added aos-cli/native_claude_config.go. Its commit message is the same diagnosis this issue reached independently:

acompose points CLAUDE_CONFIG_DIR at the session home's .claude, but the host config file lives at the home root, one level above the directory the session stages. Nothing linked it, so every native Claude session started on an empty config: no folder trust, no MCP servers, no onboarding or permission state, and no error.

It also answers the open question in the "Where CLAUDE_CONFIG_DIR gets set" section: acompose sets it, and the source was ahead of the checkout at the time.

The fix is linkNativeClaudeConfig, which is exactly the suggestion here - treat it as the same logical file whichever physical path CLAUDE_CONFIG_DIR puts it at. nativeClaudeConfigPath resolves the host spelling (scoped ~/.claude/.claude.json first, home-root ~/.claude.json otherwise), and nativeClaudeWritePath resolves the symlink before an atomic rename so a writer does not replace the link with a divergent copy.

Verified against this live session

Running the issue's own repro:

$ ls -la "$CLAUDE_CONFIG_DIR/.claude.json"
... /tmp/aos/native/pp45/home/.claude/.claude.json -> /Users/kai/.claude.json

shadow: numStartups=1151 projects=766
real:   numStartups=1151 projects=766

Not the numStartups: 1 with a single ephemeral project entry this issue recorded. The top-level home/.claude.json link the issue called dead weight is still there and still harmless.

Closing as done.

Fixed on `main`, in the shape this issue proposed. The commit referenced #956 and not this one, so it stayed open. `974c9471` ("fix(aos): project the Claude config into native session homes", 2026-08-06) added `aos-cli/native_claude_config.go`. Its commit message is the same diagnosis this issue reached independently: > acompose points `CLAUDE_CONFIG_DIR` at the session home's `.claude`, but the host config file lives at the home root, one level above the directory the session stages. Nothing linked it, so every native Claude session started on an empty config: no folder trust, no MCP servers, no onboarding or permission state, and no error. It also answers the open question in the "Where CLAUDE_CONFIG_DIR gets set" section: `acompose` sets it, and the source was ahead of the checkout at the time. The fix is `linkNativeClaudeConfig`, which is exactly the suggestion here - treat it as the same logical file whichever physical path `CLAUDE_CONFIG_DIR` puts it at. `nativeClaudeConfigPath` resolves the host spelling (scoped `~/.claude/.claude.json` first, home-root `~/.claude.json` otherwise), and `nativeClaudeWritePath` resolves the symlink before an atomic rename so a writer does not replace the link with a divergent copy. ## Verified against this live session Running the issue's own repro: ``` $ ls -la "$CLAUDE_CONFIG_DIR/.claude.json" ... /tmp/aos/native/pp45/home/.claude/.claude.json -> /Users/kai/.claude.json shadow: numStartups=1151 projects=766 real: numStartups=1151 projects=766 ``` Not the `numStartups: 1` with a single ephemeral project entry this issue recorded. The top-level `home/.claude.json` link the issue called dead weight is still there and still harmless. Closing as done.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agentic-os#878
No description provided.