Skip to content

Share Docker agent auth across containers#96

Merged
johannesjo merged 4 commits intojohannesjo:mainfrom
brooksc:docker-dotfiles
May 5, 2026
Merged

Share Docker agent auth across containers#96
johannesjo merged 4 commits intojohannesjo:mainfrom
brooksc:docker-dotfiles

Conversation

@brooksc
Copy link
Copy Markdown
Contributor

@brooksc brooksc commented May 2, 2026

Summary

Docker-mode agents currently start with a fresh writable home each time, so users have to re-sign into Claude, Codex, Gemini, OpenCode, or Copilot for every new Docker container. That repeated authentication overhead makes Docker isolation less appealing, especially when creating multiple worktrees or trying short-lived tasks.

This PR adds an opt-in setting to share agent auth across Linux containers. When enabled, the app creates a user-owned host directory under ~/.parallel-code/agent-auth/<agent> and bind-mounts it into that agent's config directory inside Docker, such as /tmp/.codex or /tmp/.claude. Signing in once inside a Docker container then persists for later Docker containers of the same agent type, including containers launched from different worktrees.

Implementation

  • Adds a persisted shareDockerAgentAuth setting and a checkbox in Settings.
  • Forwards the setting through the terminal spawn IPC path with main-process validation.
  • Mounts per-agent auth directories for known agent commands only:
    • claude -> /tmp/.claude
    • codex -> /tmp/.codex
    • gemini -> /tmp/.gemini
    • opencode -> /tmp/.config/opencode
    • copilot -> /tmp/.config/github-copilot
  • Uses host bind directories instead of Docker named volumes so the container process, which runs as the host UID/GID, can write credentials on first login.
  • Covers enabled, disabled, and unknown-agent behavior in the Docker PTY tests.

Limitations

  • This does not copy or seed credentials from existing host config directories like ~/.codex or ~/.claude; it persists credentials created inside Docker after the setting is enabled.
  • Auth is shared per agent type, not per project or worktree.
  • Only the known built-in agent command names get auth mounts. Custom or differently named wrappers are intentionally ignored unless added to the mapping.
  • Existing running containers are unaffected; the mount is applied when a Docker agent is spawned.

Validation

  • npm test -- electron/ipc/pty.test.ts
  • npm run typecheck
  • npm run lint -- --quiet
  • npm run format:check
  • Commit and push hooks also ran npm run check.

@brooksc brooksc marked this pull request as ready for review May 2, 2026 20:49
@johannesjo johannesjo requested a review from Copilot May 4, 2026 17:47
Copy link
Copy Markdown
Owner

@johannesjo johannesjo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Clean implementation — opt-in setting flows nicely from UI → store → IPC → backend, tests cover the important cases, and pre-creating the host directory to avoid root-ownership is a good detail.

Issues

1. hostDir is the same for all entries in an agent's config array (electron/ipc/pty.ts:616)

Currently every agent has exactly one config dir, but the data structure is Record<string, string[]>. If an agent ever gets multiple entries, both container paths would be backed by the same host directory, causing cross-contamination.

Suggested fix — incorporate relDir into the host path:

const hostDir = path.join(home, '.parallel-code', 'agent-auth', agentCommand, relDir);

Or simplify the type to Record<string, string> since arrays aren't actually needed today.

2. Command matching uses the raw args.command string (electron/ipc/pty.ts:567-573)

If a user specifies a full path (e.g., /usr/local/bin/claude) or the command includes arguments, the AGENT_CONFIG_DIRS lookup silently fails. Consider matching against path.basename(agentCommand) or documenting that only bare command names are supported.

Minor

  • The for...of loop over a single-element array feels like dead code — either the type should be string or the host-dir path needs to account for multiple entries (see above).

Overall this is solid and well-tested. The first point is the only one I'd want addressed before merge.

@johannesjo
Copy link
Copy Markdown
Owner

johannesjo commented May 4, 2026

Thank you very much for this! <3

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in Docker setting to persist agent authentication across container runs by threading a new preference from the UI into PTY spawn and bind-mounting per-agent auth directories inside Docker. In the broader codebase, it extends the existing Docker isolation flow so repeat agent logins are no longer required for supported CLIs.

Changes:

  • Adds a persisted shareDockerAgentAuth setting, default state, store setter, and Settings UI checkbox.
  • Forwards the new setting through renderer → IPC → PTY spawn validation.
  • Mounts per-agent auth directories for supported Docker agents and adds PTY tests for enabled/disabled/unknown-agent cases.

Review note: the feature direction makes sense, but I found blocking issues around persistence/autosave and the security/robustness of the host auth directory creation.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/store/ui.ts Adds the setter used by the new Docker auth-sharing preference.
src/store/types.ts Extends persisted/app store types with the new boolean flag.
src/store/store.ts Re-exports the new store setter.
src/store/persistence.ts Saves and restores the new preference in persisted state.
src/store/core.ts Introduces the default false value in initial app state.
src/components/TerminalView.tsx Sends the preference with SpawnAgent IPC requests.
src/components/SettingsDialog.tsx Adds the Docker auth-sharing checkbox and explanatory copy.
electron/ipc/register.ts Validates the new IPC boolean argument in the main process.
electron/ipc/pty.ts Adds supported agent config-dir mappings and Docker bind mounts for shared auth.
electron/ipc/pty.test.ts Tests shared-auth mount behavior for supported, disabled, and unknown-agent cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/store/persistence.ts
keybindingMigrationDismissed: store.keybindingMigrationDismissed || undefined,
focusMode: store.focusMode || undefined,
verboseLogging: store.verboseLogging || undefined,
shareDockerAgentAuth: store.shareDockerAgentAuth || undefined,
Comment thread electron/ipc/pty.ts Outdated
if (shareAgentAuth) {
for (const relDir of AGENT_CONFIG_DIRS[agentCommand] ?? []) {
const hostDir = path.join(home, '.parallel-code', 'agent-auth', agentCommand);
fs.mkdirSync(hostDir, { recursive: true });
Comment thread electron/ipc/pty.ts Outdated
Comment on lines +623 to +624
fs.mkdirSync(hostDir, { recursive: true });
mounts.push('-v', `${hostDir}:${DOCKER_CONTAINER_HOME}/${relDir}`);
brooksc and others added 2 commits May 4, 2026 18:20
- Use path.basename() for agent command lookup so full paths work
- Incorporate relDir into hostDir to avoid cross-contamination if an agent
  has multiple config dirs in future
- Create host auth dirs with mode 0o700 to restrict access to credentials
- Degrade gracefully if mkdirSync fails rather than aborting Docker spawn
- Add shareDockerAgentAuth to autosave snapshot so toggling persists immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brooksc brooksc force-pushed the docker-dotfiles branch from e3e5c62 to 4d12dd3 Compare May 5, 2026 01:21
brooksc and others added 2 commits May 4, 2026 19:53
Claude stores its main auth config in ~/.claude.json (at HOME level) in
addition to ~/.claude/ — without mounting this file, credentials written
in the first container are lost when the container exits and the next
container prompts for login again.

Also add a note to the new-task Docker info message when auth sharing is
enabled so users know credentials will carry over.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
An empty file causes Claude to reject it as invalid JSON on startup.
Seed it with a valid empty object so the container starts cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brooksc
Copy link
Copy Markdown
Contributor Author

brooksc commented May 5, 2026

Update

Review issues addressed

All issues raised in the review have been addressed:

  1. hostDir ignores relDir — fixed; host path now includes relDir so each config directory gets its own isolated location (e.g. agent-auth/claude/.claude/).
  2. Command matching uses raw args.command — fixed; now uses path.basename(agentCommand) so full paths like /usr/local/bin/claude work correctly.
  3. for...of over single-element array / dead code — resolved as a consequence of fix perf: optimize xterm.js rendering for Linux #1.
  4. shareDockerAgentAuth missing from autosave snapshot — fixed; toggling the checkbox now immediately schedules a save.
  5. Host auth directory created with default permissions — fixed; now uses mode: 0o700.
  6. mkdirSync throws and aborts Docker spawn — fixed; failure now logs a warning and skips the mount instead of preventing the container from starting.

Additional changes since the original PR

  • .claude.json file mount — discovered during testing that Claude stores its main auth config (OAuth tokens) in ~/.claude.json (a file at HOME level) in addition to ~/.claude/. Without mounting this file, credentials written in the first container were lost when it exited and the next container prompted for login again. Added an AGENT_CONFIG_FILES mapping alongside AGENT_CONFIG_DIRS to handle file-level mounts, seeding the host file with {} on first use so Claude doesn't reject it as invalid JSON.
  • New task dialog message — when "Share agent auth across Linux containers" is enabled, the Docker info banner now appends "Agent credentials are shared across containers."
  • Rebased onto upstream main.

Test results

Claude Code — auth persists correctly across containers; no login prompt on second container.
Codex — auth persists correctly across containers.
⚠️ Gemini — fails with uv_os_get_passwd returned ENOENT (exit code 41) before auth sharing is even reached. Root cause: Gemini CLI calls os.userInfo() to derive its keychain encryption key, and that libuv call reads /etc/passwd — which doesn't have an entry for the host UID (501) inside the container. This is unrelated to this PR and would occur with or without auth sharing enabled. Fix requires the Docker image to support arbitrary host UIDs, e.g. via fixuid or a dynamic /etc/passwd entry at container startup.
🔲 OpenCode — not tested.
🔲 Copilot — not tested.

@johannesjo
Copy link
Copy Markdown
Owner

Thank you very much!

@johannesjo johannesjo merged commit 67f3c5c into johannesjo:main May 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants