Skip to content

Sandboxed Claude session loses login after token expiry; auto-update fails #181

Closed

Description

@barelyworkingcode

Problem

A Claude session in the relay sandbox loses its login after some hours. Inside the sandbox it cannot re-authenticate. Logging in from an unsandboxed terminal and starting a new sandbox session fixes it. Auto-update also fails in the sandbox.

Root cause

Partly measured.

Measured by running the generated Claude profile (cmd/relay/testdata/golden_sandbox_profile_claude.sb) under sandbox-exec:

Path In sandbox
login keychain, read Claude Code-credentials works
login keychain, write (security add-generic-password) Operation not permitted
~/.claude/.credentials.json writable
~/.local/share/claude/versions, ~/.local/bin Operation not permitted (read-only grants)

Claude Code keeps its OAuth login (claudeAiOauth: access token, refresh token, expiresAt) in both the keychain and ~/.claude/.credentials.json. The access token lasts about 8 hours.

Hypothesis, not yet reproduced: the sandboxed client refreshes the token, the server rotates the refresh token, the keychain write is denied, and the stored refresh token is then dead. A login inside the sandbox fails at the same keychain write.

Auto-update fails because the updater must write the version directory and relink ~/.local/bin/claude. Granting those read-write would let a sandboxed agent replace the binary the next unsandboxed launch runs. Do not grant them.

Fix

Options; needs owner decision: secrets.

  • A. Credentials file only. Make the sandboxed Claude read and write ~/.claude/.credentials.json and never the keychain, so refresh survives. Open question: does Claude Code fall back to the file after a keychain write fails, or does it prefer the stale keychain copy? Settle this first.
  • B. Inject a token from relay (CLAUDE_CODE_OAUTH_TOKEN). Open question: a long-running session may go stale if the injected token does not refresh. Unknown whether it refreshes or expires fixed. Needs a measurement.
  • C. Grant write on ~/Library/Keychains. Works, but lets the sandbox alter any keychain item. Least preferred.
  • Auto-update: set DISABLE_AUTOUPDATER=1 in sandbox sessions and update from an unsandboxed terminal.

What I'd notice

Sandbox sessions stay logged in across the 8 hour token lifetime, and no update error appears at start.

Tasks

  • Reproduce: sandbox session past token expiry, record where the refresh fails (keychain write vs. file).
  • Determine whether the file store is used after a keychain write failure.
  • Determine whether an injected token refreshes in a long session.
  • Pick A, B or C; update the profile and golden test.
  • Set DISABLE_AUTOUPDATER=1 for sandbox sessions.

Contract

Login half only; the updater half shipped in relay#502. Owner decisions of 2026-10-19 are final. One PR, branch fix/181-login-store, base main.

Fix: every sandboxed Claude Code launch gets CLAUDE_CONFIG_DIR=<sandbox Claude folder>, one relay-owned folder shared by every sandboxed Claude on the Mac. Relay creates it (mode 0700) and seeds it with links to the person's ~/.claude entries, never credentials. The profile grants it read-write. Claude then keeps its login in <folder>/.credentials.json. The keychain write that the sandbox refuses no longer leaves a stale item that shadows the file: the owner measured that CLAUDE_CONFIG_DIR=<empty dir> reports loggedIn: false while the default item is readable, so Claude looks up a different keychain item for a non-default folder. Every sandboxed Claude logs out once, until the person runs /login inside one.

Acceptance criteria

  • AC1. A sandboxed claude session starts with CLAUDE_CONFIG_DIR set to the sandbox Claude folder.
  • AC2. A sandboxed terminal launch of the claude-code template starts with CLAUDE_CONFIG_DIR set to the same folder.
  • AC3. An unsandboxed launch starts without CLAUDE_CONFIG_DIR, and relay creates no sandbox Claude folder for it. An SSH host launch does not change.
  • AC4. A CLAUDE_CONFIG_DIR value in the template's own env reaches the child unchanged.
  • AC5. The profile of both launches lets the session write inside the folder, and still refuses a write beside it.
  • AC6. Relay creates the folder with mode 0700 and links each listed ~/.claude entry into it by absolute path.
  • AC7. The folder holds no .credentials.json and no .claude.json, and no entry in it is a copy of a ~/.claude file.
  • AC8. Seeding keeps an entry that already exists, link or not, and a second launch changes nothing.
  • AC9. When the folder path exists and is not a real directory, the launch is refused with 400 sandbox_unavailable, and the message names the path.
  • AC10. No keychain grant changes.

Interfaces

internal/sessions/childenv/childenv.go (stays a stdlib-only leaf; update the package doc):

const EnvClaudeConfigDir = "CLAUDE_CONFIG_DIR"
// SandboxClaudeDir is <os.UserConfigDir()>/relay/sandbox-claude: the same
// "relay" folder bridge.DefaultConfigDir names. Deliberately not bridge.ConfigDir:
// it does not move with --config-dir, so every relay instance under one HOME
// (Relay.app, each verify stack) shares one login. An os.UserConfigDir error is
// returned wrapped, never replaced by a fallback.
func SandboxClaudeDir() (string, error)
// SandboxedClaude now takes the folder: {DISABLE_AUTOUPDATER: "1", CLAUDE_CONFIG_DIR: configDir}.
func SandboxedClaude(configDir string) map[string]string

cmd/relay/session_claude_dir.go (new):

// isClaudeLaunch: kind == KindClaude, or kind == KindPTY and tmpl.ID == kindTemplateIDs[KindClaude].
func isClaudeLaunch(kind string, tmpl *config.TerminalTemplate) bool
// ensureSandboxClaudeDir resolves SandboxClaudeDir, creates it and seeds it; returns the folder.
func ensureSandboxClaudeDir() (string, error)

ensureSandboxClaudeDir behaviour:

  1. os.MkdirAll the parent (0700), then os.Mkdir(dir, 0o700). If it exists, os.Lstat must report a directory, not a link: otherwise return sandbox claude dir <path>: not a directory. An existing directory keeps its mode.
  2. For each name in sandboxClaudeLinks: if os.Lstat(<dir>/<name>) finds anything, skip it (never overwrite, links included). Target is <home>/.claude/<name> from os.UserHomeDir, not symlink-resolved. projects and sessions get their target created first (MkdirAll, 0700). Every other name is linked only when its target exists. os.Symlink(target, link); EEXIST counts as seeded (a parallel launch got there first); any other error returns sandbox claude dir <path>: link <name>: <err>.
  3. It never reads, copies or opens a file in ~/.claude.

sandboxClaudeLinks, each with its reason:

  • settings.json: the person's permissions, hooks, env and model choice, as an unsandboxed Claude sees them.
  • CLAUDE.md: the person's global instructions; a sandboxed Claude reads them today through ~/.claude.
  • skills, agents, commands: the person's own skills, subagents and slash commands.
  • projects: transcripts. Relay reads history on join and deletes transcripts under ~/.claude/projects (ReadClaudeHistory, DeleteSession), and the outside-session probe reads there too.
  • sessions: Claude's pid registry. The outside-session probe and devboxverify read ~/.claude/sessions.
  • Never linked: .credentials.json (the login itself); .claude.json, which lives in the folder under CLAUDE_CONFIG_DIR and can hold account data or an API key. Plugins and the rest stay out (Q4).

cmd/relay/session_launch.go, AuthorizeLaunch, right after sandbox is computed: when sandbox && isClaudeLaunch(req.Kind, tmpl), call ensureSandboxClaudeDir; an error returns invalidRequest("sandbox_unavailable", err.Error(), baseFields) (an existing, documented code: audited and evented as today). The PTY block from relay#502 passes that folder to childenv.SandboxedClaude(dir), and a key the template's env sets still wins.

cmd/relay/session_sandbox.go, sandboxSpecForLaunch: when isClaudeLaunch(kind, tmpl), append childenv.SandboxClaudeDir() to readWrite, beside the pi leaf. This is not demoted in a read-only-projects session, because it is not a project. sandboxWritableRoots needs no change: the folder is under home, which is always in W.

internal/sessions/provider/claude.go, Start (console spawn): when p.cfg.SandboxProfile != "", resolve childenv.SandboxClaudeDir(). An error fails the start with sandboxed claude: %w. buildClaudeEnv merges childenv.SandboxedClaude(dir). Relay already created the folder in AuthorizeLaunch (a resume goes through it too), so the provider creates nothing.

Wire shapes: none change. No hostapi field; both processes resolve the same function under the same HOME, and the e2e path assertions catch any drift.

Paths

  • Simple: the person opens a Claude Code terminal (eve terminal card, relay sandbox claude-code) or starts a Claude session. The first sandboxed Claude asks for a login. The person runs /login once inside it, and every later sandboxed Claude on the Mac shares that login and refreshes it. No new screen, no new choice.
  • Power: settings.json terminal_templates[] claude-code env.CLAUDE_CONFIG_DIR points a terminal at another folder, which the operator must also grant in read_write. A claude session reads no template env, so it has no power door. That is deliberate: one store for every session.

File ownership

Dev task (A):

  • internal/sessions/childenv/childenv.go
  • internal/sessions/provider/claude.go
  • cmd/relay/session_claude_dir.go (new)
  • cmd/relay/session_launch.go
  • cmd/relay/session_sandbox.go
  • internal/sessions/sandbox/sandbox.go: package comment only. The ~/Library/Keychains sentence no longer holds for Claude's login; it becomes: kept readable, but the login no longer depends on it.
  • docs/session-host.md: a new subsection after the updater paragraph covering the folder, who makes it, the link list, /login once, the keychain untouched, template env wins, and unsandboxed and SSH host launches unchanged. Also extend the "Every session" grant row: a Claude launch also gets the sandbox Claude folder.
  • docs/FEATURES.md: a new row G1.44 "Sandboxed Claude keeps its own login". Simple door: none (turns on by itself; /login once). Power door: settings.json terminal_templates[].env. Doors as G1.43. Proof: event:session.launch=ok#session_id code:http:POST /api/sessions#400. Tests: the nine names below.

Test task (B):

  • e2e/features/bug181_sandboxed_claude_login_test.go (new; reuses b181Start and b181Terminal from the updater file, which it does not edit)
  • e2e/fakes/fakeagent/main.go: add CLAUDE_CONFIG_DIR to watchedEnv, nothing else
  • docs/testing.md: the watched-variable sentence names CLAUDE_CONFIG_DIR

No other file. docs/THREAT-MODEL.md does not change: a sandboxed Claude reaches a Claude login it already reaches today (keychain read, ~/.claude read-write), now in a relay-owned leaf. No promise moves; the reviewer confirms. No golden profile test exists (relay has no unit tests); AC5 is proven by behaviour.

Tests (task B), expected folder W = <i.Home>/Library/Application Support/relay/sandbox-claude

  1. TestBug181SandboxedClaudeSessionConfigDir: the session's start line has CLAUDE_CONFIG_DIR == W (AC1).
  2. TestBug181SandboxedClaudeTerminalConfigDir: b181Terminal env has CLAUDE_CONFIG_DIR == W (AC2).
  3. TestBug181UnsandboxedClaudeNoConfigDir: with sandbox:false, the key is unset and W does not exist (AC3).
  4. TestBug181TemplateConfigDirWins: template env CLAUDE_CONFIG_DIR=<i.Tmp>/acme-claude reaches the child as that value (AC4).
  5. TestBug181SandboxClaudeDirWritable: in a session, !sh touch "<W>/acme-probe" answers exit: 0 and the file exists. !sh touch "<i.Home>/Library/Application Support/relay/acme-outside" does not answer exit: 0, and no file appears (AC5). Quote the paths: they contain a space.
  6. TestBug181SandboxClaudeTerminalDirWritable: override the claude-code template's command with /bin/sh -c 'touch "$CLAUDE_CONFIG_DIR/acme-probe"'. relay sandbox claude-code from the project folder exits 0 and the file exists in W (AC2, AC5).
  7. TestBug181SandboxClaudeDirSeeded: the fixture ~/.claude holds settings.json, CLAUDE.md, skills/, agents/, commands/, plus a .credentials.json and a ~/.claude.json of placeholder bytes (acme-placeholder, never a token). After one session turn: W is mode 0700. Each of the seven names is a link whose Readlink is <i.Home>/.claude/<name>, and the projects and sessions targets are directories. W holds no .credentials.json and no .claude.json. No regular file in W holds the placeholder bytes, and every entry in W is one of the seven (AC6, AC7).
  8. TestBug181SandboxClaudeDirKeepsOwnEntries: W exists before the launch with a regular settings.json (acme-own). Two launches both answer 201. settings.json stays a regular file with the same bytes, and the other links match after each launch (AC8).
  9. TestBug181SandboxClaudeDirNotADirRefused: W is a symlink to <i.Home>/acme-elsewhere. POST /api/sessions answers 400 with code sandbox_unavailable and a message containing W, and acme-elsewhere stays empty (AC9).

AC10: review only. The diff changes no read entry and no keychain path.

Test surface

  • e2e (integration through HTTP and CLI): AC1 to AC9.
  • Playwright: none, because no UI changes.
  • Real-app check (test machine, needs no account): build the head, then start a sandboxed claude-code terminal. Before (main): claude auth status inside reads the stale store. After (head): CLAUDE_CONFIG_DIR is the folder, the links are present, and claude auth status reports loggedIn: false, the decided one-time logout.
  • Interactive, owner only, not a merge condition: /login inside a sandboxed Claude, then a check past one token refresh.

Waits

  • Task A: Waits: none; it adds no async work. Seeding finishes inside AuthorizeLaunch before the launch response.
  • Task B: Waits: boot on harness.Start (ready.json) and WaitSessionHost. A session launch waits on its 201, a turn on the session message reply (the start line is written before the first reply). A terminal waits on the session.exited event for its id. relay sandbox waits on the CLI process exit. A refusal waits on the HTTP response. Never a sleep.

Budget

  • A: 8 files, about 160 lines (code about 110, docs about 50).
  • B: 3 files, about 260 lines.
  • Single PR, about 420 lines. Past 11 files or 550 lines is a question for the coordinator.

Tasks

  • A: dev, as above.
  • B: tests 1 to 9. Red on main: test 1 fails at its assertion on main (CLAUDE_CONFIG_DIR unset) through POST /api/sessions.
  • Verification task (last; no devlock): repo-suite (cd e2e && go test -race ./features/). Then devboxverify: stack-up r181 --relay <head checkout> --eve <eve main checkout>, go run ./cmd/devboxverify --stack <stack.json> --phase api, stack-down r181. Then the real-app check above. Post Verified: <head> · repo-suite: … · devboxverify: … · real-app: … · log: …. The login journeys are expected to fail until the owner's /login (Q2).

Open questions (recommendation first)

  1. INSIDE. Should the grant go in code? Yes, code, as with the pi leaf. The env value must be code anyway: env values do not expand ~, and a claude session reads no template env. The grant could only go in the template through a settings migration that rewrites every install's claude-code template. That is a larger diff, it edits the person's config, and it lets the env and the grant disagree.
  2. BEYOND. This PR's stack run, and every later verify on the machine, fails the five login journeys until the owner runs /login once inside a sandboxed Claude on the test machine. Recommendation: the PR records them as an expected known defect pointing at this issue, with no harness change. Pause the nightly (its pause file) from merge until that /login, so a RED night from those journeys does not trigger Revert first.
  3. INSIDE. Should projects and sessions be linked, beyond the owner's list? Yes. Without them, history on join, transcript delete and the outside-session probe lose sandboxed sessions. Neither holds credentials.
  4. BEYOND. Should plugins be linked? No, not in this PR; it is outside the owner's list. A sandboxed Claude loses the person's plugins until a follow-up is approved. File a from-dev idea if anyone misses them.
  5. INSIDE. Should seeding replace a link that points elsewhere (one a session planted)? No: never overwrite, links included. Such a link only reaches what the profile already grants.

ship-blocker: no (owner decision 2026-10-10: not a regression; a known issue until the owner has a working sandbox login)

Activity

  1. barelyworkingcode commented on Oct 1, 2026

    @barelyworkingcode
    OwnerAuthor

    Update from reading Claude Code 2.1.286's credential storage. The root cause is now much firmer, and option A needs no flag.

    Mechanism. Storage is "keychain with plaintext fallback". Read: keychain first, and the file only if the keychain has no item. Write: try the keychain. If that fails for a non-transient reason, write ~/.claude/.credentials.json and try to delete the keychain item.

    In the sandbox the keychain write is denied (Operation not permitted), so the refreshed tokens land in the file. The keychain delete is denied too, so the old item stays. The next read takes the stale keychain item over the fresh file. That is the stale login. A login inside the sandbox fails the same way.

    Measured. With the ~/Library/Keychains read grant removed from the profile, security find-generic-password inside the sandbox exits 44 ("item not found"). Claude Code reads that as an absent item and falls through to the file.

    Fix (option A). Drop ~/Library/Keychains from the Claude template's read list. That tightens the profile. ~/.claude is already read-write, so the file needs no new grant and no flag.

    Caveats.

    • sandbox.go says the keychain "must stay readable for Claude Code to stay logged in". That holds only when the keychain is the sole store. The file must be populated first.
    • The sandbox and the unsandboxed client then hold separate copies of the login. A refresh on one may invalidate the other's refresh token. Needs measuring before the fix ships.
    • No change to DISABLE_AUTOUPDATER guidance above.
  2. barelyworkingcode commented on Oct 1, 2026

    @barelyworkingcode
    OwnerAuthor

    Better fix than removing the keychain grant: there is an env var, not a flag. CLAUDE_SECURESTORAGE_CONFIG_DIR=<dir> in sandbox sessions.

    In 2.1.286 it sets the credential file directory (<dir>/.credentials.json) and suffixes the keychain service name with a hash of <dir>. The sandbox then looks up a keychain item that does not exist, so the stale Claude Code-credentials item can no longer shadow the file. Pointing it at ~/.claude keeps the file where it already is, and the profile needs no change. There is no flag that forces the file alone.

    Measured, with claude auth status:

    • default: loggedIn: true
    • env set to ~/.claude, inside the sandbox profile: loggedIn: true (read from the file)
    • env set to an empty directory: loggedIn: false, so the variable does redirect storage

    Not yet measured: a real token refresh through this path, and whether the sandbox login and the unsandboxed login can refresh independently. Safest seeding is a separate /login inside the sandbox, which gives it its own grant rather than a copy of the host's refresh token.

  3. barelyworkingcode commented on Oct 10, 2026

    @barelyworkingcode
    OwnerAuthor

    Owner decision (2026-10-10, item 1, intent; the owner removed needs-review): approved. Fix goes in the bug bash epic, relay#339, as its first item. It touches credentials (item 3): the owner signed this one off, and the PR still goes through merge-check holds for the secrets path.

  4. added
    ship-blockerMust be fixed before ship; merge-check --ship refuses while one is open
    on Oct 10, 2026
  5. barelyworkingcode commented on Oct 10, 2026

    @barelyworkingcode
    OwnerAuthor

    Decisions made for the owner

    • Scope: both sandboxed Claude launch paths get the env: a claude session and a sandboxed terminal launch of the claude-code template (eve terminal card, relay sandbox claude-code). An operator's own template that runs claude under another id can set the two keys in its env; a key the template sets wins.
    • CLAUDE_SECURESTORAGE_CONFIG_DIR is <home>/.claude from os.UserHomeDir, not symlink-resolved, so Claude's hashed keychain service name stays stable. A home-dir error refuses the launch.
    • Contract amendments (test-only): internal/sessions/childenv (existing leaf package) holds the two names for both call sites; e2e/fakes/fakeagent/claude.go exits after its start line on a tty so terminal tests wait on session.exited instead of polling.
    • Not measured here: a real 8-hour refresh through the file. Seeding: run /login inside the sandbox if the file has no login (docs/session-host.md).
  6. added a commit that references this issue on Oct 10, 2026
  7. barelyworkingcode commented on Oct 10, 2026

    @barelyworkingcode
    OwnerAuthor

    Reopened: PR #360 is being reverted (PR #366, bug #365) because the epic's full verify on main went red. Not re-landed in this pass (owner's watcher, board #171).

    Findings

    • devboxverify --epic 339 on main 9ae076f, Relay.app rebuilt from it: every journey running a local sandboxed Claude session failed: session-agent-state, session-drop-in, chief-of-staff-send, cos-start FAIL; session-drop-in-tool-refused BLOCKED. Each reads as a turn that never reaches idle. cos-start-host (SSH host, not sandboxed) and every other journey passed.
    • With CLAUDE_SECURESTORAGE_CONFIG_DIR=~/.claude, a sandboxed Claude reads its login only from ~/.claude/.credentials.json. On the test machine that file holds a claudeAiOauth entry whose expiresAt is missing: a stale copy, not the live login, which sits in the keychain. Likely the sandboxed sessions ran with a dead login. Not confirmed (no session transcript inspected).
    • The e2e tests use the fake claude, so they proved the env reached the child, not that a real Claude stays logged in. A re-land needs a real-Claude check: a devbox journey or the full verify.

    What a re-land needs (proposal for the owner)

    1. A seeded, live login in the file before the switch: one /login inside the sandbox (its own grant), or relay refusing to set the variable when the file has no live login. Which one changes what a user sees, so it is an owner decision.
    2. The full verify green with the change, before merge.
    3. The open measurement from the issue: a real token refresh through the file.
  8. barelyworkingcode commented on Oct 10, 2026

    @barelyworkingcode
    OwnerAuthor

    Owner decision (2026-10-10): stage one. Sandboxed Claude launches get the documented CLAUDE_CONFIG_DIR (one shared relay-owned dir, seeded with links to settings, skills, agents and commands, never credentials). The person runs /login once inside the sandbox. PR 360 relied on the undocumented CLAUDE_SECURESTORAGE_CONFIG_DIR and was reverted after the full verify. Journeys that need a logged-in sandboxed Claude report blocked until a manual login; the owner tests the login by hand. Issue stays open with ship-blocker. A larger isolation design (separate user, Apple containers) is being researched by the owner and is not part of this.

  9. barelyworkingcode commented on Oct 11, 2026

    @barelyworkingcode
    OwnerAuthor

    Taken out of epic relay#339 by the owner (2026-10-10); tracked on its own as a ship-blocker. Stage one is in progress on branch fix/181-claude-config-dir.

  10. barelyworkingcode commented on Oct 11, 2026

    @barelyworkingcode
    OwnerAuthor

    Owner decision (2026-10-10): options 4 and 2. DISABLE_AUTOUPDATER=1 and the documented CLAUDE_CONFIG_DIR go in as configuration (the claude-code template's env and read_write in settings.json) wherever the config path can carry them; code only for what it cannot. The five journeys that need a login report not-run, not blocked. The PR may merge without the owner's login. This issue stays open with ship-blocker until the owner tests a sandbox login by hand.

  11. barelyworkingcode commented on Oct 11, 2026

    @barelyworkingcode
    OwnerAuthor

    Owner decision (2026-10-10): no work on this issue for now. The owner is testing the sandbox login by hand on his Mac and will return with a working solution; then this bug is picked up. Stage one (documented CLAUDE_CONFIG_DIR, DISABLE_AUTOUPDATER, journeys not-run without a login) is cancelled and nothing is built. PR 360 stays reverted. The issue stays open and labelled ship-blocker, so relay does not read as shippable until it is resolved. Findings so far: CLAUDE_SECURESTORAGE_CONFIG_DIR is undocumented; CLAUDE_CONFIG_DIR is the documented per-process lever; the sandboxed Claude cannot refresh a login it cannot write; a same-user process can read another process's environment (tested), so a token in env is not private.

  12. barelyworkingcode commented on Oct 11, 2026

    @barelyworkingcode
    OwnerAuthor

    Stage one is cancelled (owner, 2026-10-10). No branch or PR exists. The owner is testing the sandbox by hand and will return to this bug with a working solution. It stays open with ship-blocker and gets no work until then.

  13. removed
    ship-blockerMust be fixed before ship; merge-check --ship refuses while one is open
    on Oct 11, 2026
  14. barelyworkingcode commented on Oct 11, 2026

    @barelyworkingcode
    OwnerAuthor

    Owner decision (2026-10-10): not a ship-blocker for this ship. The bug is not a regression: the sandbox works today with a login inside it, and the owner tests that by hand. The issue stays open as a known issue and is picked up when the owner has a working solution. Label ship-blocker removed on the owner's instruction.

  15. barelyworkingcode commented on Oct 15, 2026

    @barelyworkingcode
    OwnerAuthor

    Groomed (coordinator, 2026-10-14): Renamed ## Root cause (partly measured) and ## Fix options (needs owner decision: secrets) to ## Root cause and ## Fix; each qualifier moved to the first line of its section. No change of meaning.

  16. barelyworkingcode commented on Oct 15, 2026

    @barelyworkingcode
    OwnerAuthor

    Groomed (coordinator, 2026-10-14): added ship-blocker: no against the ship-blocker criteria in the flow rules (a security hole inside the threat model that real use can reach; loss of a user's work, data or login; a defect in a must-have feature group). This restates the owner's recorded decision of 2026-10-10; it is not a new one. The owner can flip it. No other change.

  17. barelyworkingcode commented on Oct 19, 2026

    @barelyworkingcode
    OwnerAuthor

    Parked (coordinator, 2026-10-18): the login half. The auto-update half moved to relay#500 and ships on its own.

    Confirmed on main 9ff41de

    • Under a profile relay rendered for a live claude-code session, a keychain write gets Operation not permitted; a keychain read works; ~/.claude is writable.
    • On the test machine, ~/.claude/.credentials.json holds a claudeAiOauth entry with empty tokens, written today. The live login is in the keychain. This matches the mechanism above: the sandbox writes the file, and reads the stale keychain.
    • CLAUDE_CONFIG_DIR=<empty dir> claude auth status reports loggedIn: false on Claude Code 2.1.303. The documented lever does give a separate store.

    Why it is parked

    Every storage switch makes existing sandboxed Claude sessions log out until someone runs /login inside the sandbox. PR 360 did this and was reverted after the full verify. Proving any fix needs that /login with your account and an 8-hour refresh. A run cannot do either.

    What would unblock it

    1. Pick the store: a relay-owned CLAUDE_CONFIG_DIR (documented; settings links seeded) or CLAUDE_SECURESTORAGE_CONFIG_DIR=~/.claude (undocumented).
    2. Run /login once inside a sandboxed Claude on the test machine, so the five login journeys have a live login.
    3. Leave it running past one token refresh, then report claude auth status.
  18. barelyworkingcode commented on Oct 19, 2026

    @barelyworkingcode
    OwnerAuthor

    The auto-update half shipped in relay#502 (closes relay#500). The login half stays open and parked; see the comment above.

  19. added a commit that references this issue on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions