diff --git a/.env.example b/.env.example index 125ad43e98..f7484deb33 100644 --- a/.env.example +++ b/.env.example @@ -187,14 +187,27 @@ GITEA_ALLOWED_USERS= # Archon Directory Configuration # ============================================ # All Archon-managed files go in ~/.archon/ by default -# Override with ARCHON_HOME to use a custom location +# Override with ARCHON_HOME to use a custom location. +# Docker: IGNORED. The container always uses /.archon regardless of this value +# (the variable still leaks into the container env via env_file but has no effect). # ARCHON_HOME=~/.archon # Docker data directory (host path where Archon stores workspaces, worktrees, artifacts, etc.) # Default: Docker-managed volume (archon_data) # Set to an absolute path on the host for full control over data location: +# Docker: host-only. Used by docker-compose to choose the bind-mount source for /.archon. +# NOT read by Archon source code — the container always sees data at /.archon. # ARCHON_DATA=/opt/archon-data +# Docker user-home directory (host path for /home/appuser inside the container). +# /home/appuser is persisted by default so Claude Code skills/commands/agents/hooks, +# Codex/Pi auth state, ~/.gitconfig, and shell history survive container rebuilds. +# Default: Docker-managed volume (archon_user_home) +# Set to an absolute path on the host to bind-mount instead (must be writable by UID 1001): +# Docker: host-only. Used by docker-compose to choose the bind-mount source for /home/appuser. +# NOT read by Archon source code. +# ARCHON_USER_HOME=/opt/archon-user-home + # Logging (optional) # Set log level: fatal | error | warn | info | debug | trace # Default: info diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b90c77b9..c6f67b17a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Docker: `/home/appuser` is now persisted by default via the `archon_user_home` named volume, so user-installed Claude Code skills/commands/agents/hooks, Codex/Pi auth, `~/.gitconfig`, and shell history survive container rebuilds. Set `ARCHON_USER_HOME=/host/path` in `.env` to bind-mount a host path instead (#1517, #1518). + +### Changed + +- Claude provider default `settingSources` changed from `['project']` to `['project', 'user']`, so skills, commands, agents, and `CLAUDE.md` from `~/.claude/` are now loaded by default in all environments — not just Docker. Without this, the new `/home/appuser` persistence would not actually surface user-installed Claude resources. Set `assistants.claude.settingSources: ['project']` in `.archon/config.yaml` to restore the previous project-only behavior (#1518). +- `.env.example`, `docker-compose.yml`, `deploy/docker-compose.yml`, and `reference/configuration.md` now document that `ARCHON_HOME` is silently overridden inside Docker and `ARCHON_DATA` is a Compose-only host token never read by source. The Docker entrypoint emits a one-line stderr warning when either is set in the container env (#1517). + +### Fixed + +- Docker: `git config --global --add safe.directory` in the entrypoint now de-duplicates entries before adding, preventing unbounded growth of `~/.gitconfig` now that `/home/appuser` is persisted (#1518). +- Docker: `setup-auth` now warns at startup when `CODEX_*` env vars are absent but a persisted `~/.codex/auth.json` from a previous run still exists, so operators don't accidentally use stale or revoked credentials (#1518). + ## [0.3.10] - 2026-04-29 Maintainer workflow suite, loop output variables, and broad workflow engine fixes diff --git a/CLAUDE.md b/CLAUDE.md index 75ec512975..81ac7f9de3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -479,9 +479,9 @@ The system supports configuring default models and options per assistant in `.ar assistants: claude: model: sonnet # or 'opus', 'haiku', 'claude-*', 'inherit' - settingSources: # Controls which CLAUDE.md files Claude SDK loads - - project # Default: only project-level CLAUDE.md - - user # Optional: also load ~/.claude/CLAUDE.md + settingSources: # Controls which CLAUDE.md, skills, commands, and agents the SDK loads + - project # Project-level /.claude/ (included in default) + - user # User-level ~/.claude/ (included in default; omit both to restrict to project-only) claudeBinaryPath: /absolute/path/to/claude # Optional: Claude Code executable. # Native binary (curl installer at # ~/.local/bin/claude) or npm cli.js. diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 6529d6c2e9..3153af0696 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -21,6 +21,7 @@ services: - "${PORT:-3000}:${PORT:-3000}" volumes: - ${ARCHON_DATA:-archon_data}:/.archon + - ${ARCHON_USER_HOME:-archon_user_home}:/home/appuser healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${PORT:-3000}/api/health"] interval: 30s @@ -46,4 +47,5 @@ services: volumes: archon_data: + archon_user_home: # postgres_data: diff --git a/docker-compose.override.example.yml b/docker-compose.override.example.yml index 545b121644..aa2760cf41 100644 --- a/docker-compose.override.example.yml +++ b/docker-compose.override.example.yml @@ -14,3 +14,8 @@ services: build: context: . dockerfile: Dockerfile.user + +# /home/appuser (Claude/Codex/Pi config, gitconfig, shell history) is already +# persisted by default via the archon_user_home named volume in the base compose. +# To bind-mount a host path instead, set ARCHON_USER_HOME=/your/host/path in .env +# (the host path must be writable by UID 1001). No override-file edit needed. diff --git a/docker-compose.yml b/docker-compose.yml index e1b4290e3c..45a24a4dd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,13 @@ # ARCHON_DATA=/opt/archon-data # Any absolute path on the host # Default: Docker-managed volume (archon_data) # +# User home (Claude/Codex/Pi config, gitconfig, shell history): +# /home/appuser is persisted by default to the archon_user_home named volume so +# user-installed Claude Code skills/commands/agents/hooks, Codex/Pi auth state, +# and ~/.gitconfig survive container rebuilds. To use a host path instead: +# ARCHON_USER_HOME=/opt/archon-user-home # Any absolute path on the host +# Default: Docker-managed volume (archon_user_home) +# # Cloud (HTTPS): # 1. Set DOMAIN=archon.example.com in .env # 2. Point DNS A record to your server @@ -39,6 +46,7 @@ services: - "${PORT:-3000}:${PORT:-3000}" volumes: - ${ARCHON_DATA:-archon_data}:/.archon + - ${ARCHON_USER_HOME:-archon_user_home}:/home/appuser networks: - archon-network restart: unless-stopped @@ -122,6 +130,7 @@ services: volumes: archon_data: + archon_user_home: postgres_data: caddy_data: caddy_config: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 26b9aee024..1dc4dd234c 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -13,12 +13,30 @@ if [ "$(id -u)" = "0" ]; then echo "ERROR: Failed to fix ownership of /.archon — volume may be read-only or mounted with incompatible options" >&2 exit 1 fi + # /home/appuser is persisted to a named volume (or bind-mounted via + # ARCHON_USER_HOME) so Claude/Codex/Pi config, ~/.gitconfig, shell history, + # and other user-specific state survive rebuilds. On bind mounts, host UIDs + # don't map to appuser (1001), so fix ownership the same way we do /.archon. + if ! chown -Rh appuser:appuser /home/appuser 2>/dev/null; then + echo "ERROR: Failed to fix ownership of /home/appuser — volume may be read-only or mounted with incompatible options" >&2 + exit 1 + fi RUNNER="gosu appuser" else # Already running as non-root (e.g., --user flag or Kubernetes) RUNNER="" fi +# Warn if vars known to be ignored inside the container were set via env_file: .env. +# These leak in but have no effect (ARCHON_HOME is overridden to /.archon by source; +# ARCHON_DATA is a host-side compose substitution token, never read by the container). +if [ -n "${ARCHON_HOME:-}" ]; then + echo "[archon] ARCHON_HOME=${ARCHON_HOME} ignored in Docker (container home is fixed at /.archon)" >&2 +fi +if [ -n "${ARCHON_DATA:-}" ]; then + echo "[archon] ARCHON_DATA=${ARCHON_DATA} is a host-side compose token; not read inside the container" >&2 +fi + # Register all git repositories under /.archon as safe directories. # Git 2.35.2+ (CVE-2022-24765) rejects repos owned by a different UID. # On macOS bind mounts (VirtioFS), host UIDs don't map to appuser (1001), @@ -26,8 +44,14 @@ fi # The Dockerfile RUN-layer registers fixed paths, but that gitconfig lives # in the image layer — bind mounts don't inherit it on restart, and # worktrees are nested at arbitrary depths unknown at build time. +# With /home/appuser now persisted, ~/.gitconfig survives across restarts — +# so we must check before --add or duplicate safe.directory lines accumulate +# every boot. find /.archon -name ".git" -prune -print 2>/dev/null | while IFS= read -r git_dir; do - $RUNNER git config --global --add safe.directory "$(dirname "$git_dir")" + repo_dir="$(dirname "$git_dir")" + if ! $RUNNER git config --global --get-all safe.directory 2>/dev/null | grep -qxF "$repo_dir"; then + $RUNNER git config --global --add safe.directory "$repo_dir" + fi done # Configure git to use GH_TOKEN for HTTPS clones via credential helper diff --git a/packages/docs-web/src/content/docs/deployment/docker.md b/packages/docs-web/src/content/docs/deployment/docker.md index e1caf127a7..3897f6ef38 100644 --- a/packages/docs-web/src/content/docs/deployment/docker.md +++ b/packages/docs-web/src/content/docs/deployment/docker.md @@ -452,6 +452,10 @@ By default this is a Docker-managed volume. To store data at a specific location ARCHON_DATA=/opt/archon-data ``` +:::note +`ARCHON_HOME` from `.env.example` is **ignored inside Docker** — the container always uses `/.archon`. Use `ARCHON_DATA` (host-side bind-mount source) to control *where on the host* `/.archon` lives. Both `ARCHON_HOME` and `ARCHON_DATA` leak into the container env via `env_file: .env`, which is harmless but expected. +::: + The directory is created automatically. Make sure the path is writable by UID 1001 (the container user): ```bash @@ -461,6 +465,40 @@ sudo chown -R 1001:1001 /opt/archon-data If `ARCHON_DATA` is not set, Docker manages the volume automatically (`archon_data`) — data persists across restarts and rebuilds but lives inside Docker's storage. +### User Home Directory (Persisted) + +The container runs as `appuser` with `$HOME=/home/appuser`. The base compose mounts `/home/appuser` as a named volume (`archon_user_home`) by default, so user-specific state survives container rebuilds without any operator action: + +| Path | What it persists | +|------|------------------| +| `~/.claude/` | Claude Code skills, commands, agents, hooks, MCP config, projects (conversation history), memory, OAuth state, keybindings, file-history | +| `~/.codex/` | Codex auth (`auth.json` from interactive `codex login`; the env-var path via `setup-auth` overwrites this on every container start) | +| `~/.pi/agent/` | Pi `auth.json` from interactive `pi /login` (Archon's Pi adapter reads this on every request) | +| `~/.gitconfig` | Author identity, signing config, custom aliases, plus the `safe.directory` entries baked into the image | +| `~/.bash_history` | Shell history when you `docker compose exec app bash` | +| `~/.config/gh/` | GitHub CLI auth from interactive `gh auth login` (the `GH_TOKEN` env-var path works without it) | + +To bind-mount a host path instead of the default named volume, set `ARCHON_USER_HOME` in `.env`: + +```ini +ARCHON_USER_HOME=/opt/archon-user-home +``` + +The host path must be writable by UID 1001 — chown it once before first start: + +```bash +mkdir -p /opt/archon-user-home +sudo chown -R 1001:1001 /opt/archon-user-home +``` + +The entrypoint re-applies ownership on every container start, so subsequent rebuilds work without re-running `chown`. + +:::caution +Bind-mount paths do **not** inherit the image's baked `~/.gitconfig` (Docker only copies image content into named volumes on first creation, never into bind mounts). The entrypoint still registers git `safe.directory` entries for `/.archon/workspaces` and `/.archon/worktrees` repos at runtime, so functionality is preserved — but a bind-mounted `~/.gitconfig` starts empty and any author identity / signing config you want must be set explicitly with `git config --global` inside the container. +::: + +If `ARCHON_USER_HOME` is not set, Docker manages the volume automatically (`archon_user_home`) — config persists across restarts and rebuilds but lives inside Docker's storage. To wipe it: `docker compose down && docker volume rm archon_archon_user_home`. + ### GitHub CLI Authentication `GH_TOKEN` from `.env` is picked up automatically. Alternatively: diff --git a/packages/docs-web/src/content/docs/getting-started/ai-assistants.md b/packages/docs-web/src/content/docs/getting-started/ai-assistants.md index de4004a6ba..cbd9d35d5d 100644 --- a/packages/docs-web/src/content/docs/getting-started/ai-assistants.md +++ b/packages/docs-web/src/content/docs/getting-started/ai-assistants.md @@ -127,7 +127,7 @@ assistants: # claudeBinaryPath: /absolute/path/to/claude ``` -The `settingSources` option controls which `CLAUDE.md` files the Claude Code SDK loads. By default, only the project-level `CLAUDE.md` is loaded. Add `user` to also load your personal `~/.claude/CLAUDE.md`. +The `settingSources` option controls which `CLAUDE.md`, skill, command, and agent files the Claude Code SDK loads. The default is `['project', 'user']`, which loads both the project-level `/.claude/` and your personal `~/.claude/`. Set it to `['project']` if you want to scope a workflow to project-only resources. ### Set as Default (Optional) diff --git a/packages/docs-web/src/content/docs/guides/skills.md b/packages/docs-web/src/content/docs/guides/skills.md index 667f88562f..40e8ef32eb 100644 --- a/packages/docs-web/src/content/docs/guides/skills.md +++ b/packages/docs-web/src/content/docs/guides/skills.md @@ -123,14 +123,17 @@ Step-by-step content here. The agent loads this when the skill activates. ## Skill Discovery -Skills are discovered from these locations (via `settingSources: ['project']` -set in ClaudeProvider): +Skills are discovered from these locations (via the default +`settingSources: ['project', 'user']` set in ClaudeProvider): | Location | Scope | |----------|-------| | `.claude/skills/` (in cwd) | Project-level | | `~/.claude/skills/` | User-level (all projects) | +Set `assistants.claude.settingSources: ['project']` in `.archon/config.yaml` +to scope a workflow to project-level skills only. + Skills installed via `npx skills add` land in `.claude/skills/` by default. Use `-g` for global installation to `~/.claude/skills/`. diff --git a/packages/docs-web/src/content/docs/reference/configuration.md b/packages/docs-web/src/content/docs/reference/configuration.md index d312c734a2..1800c69e84 100644 --- a/packages/docs-web/src/content/docs/reference/configuration.md +++ b/packages/docs-web/src/content/docs/reference/configuration.md @@ -62,9 +62,9 @@ defaultAssistant: claude # must match a registered provider (e.g. claude, codex) assistants: claude: model: sonnet - settingSources: # Which CLAUDE.md files the SDK loads (default: ['project']) - - project # Project-level CLAUDE.md (always recommended) - - user # Also load ~/.claude/CLAUDE.md (global preferences) + settingSources: # Which sources the Claude SDK loads (default: ['project', 'user']) + - project # Project-level /.claude/ (CLAUDE.md, skills, commands, agents) + - user # User-level ~/.claude/ (CLAUDE.md, skills, commands, agents) # Optional: absolute path to the Claude Code executable. # Required in compiled Archon binaries when CLAUDE_BIN_PATH is not set. # Accepts the native binary (~/.local/bin/claude from the curl installer) @@ -153,25 +153,25 @@ defaults: ### Claude settingSources -Controls which `CLAUDE.md` files the Claude Agent SDK loads during sessions: +Controls which sources the Claude Agent SDK loads during sessions — `CLAUDE.md`, skills, commands, agents, and hooks: | Value | Description | |-------|-------------| -| `project` | Load the project's `CLAUDE.md` (default, always included) | -| `user` | Also load `~/.claude/CLAUDE.md` (user's global preferences) | +| `project` | Load project-level `/.claude/` (CLAUDE.md, skills, commands, agents) | +| `user` | Load user-level `~/.claude/` (CLAUDE.md, skills, commands, agents) | -**Default**: `['project']` -- only project-level instructions are loaded. +**Default**: `['project', 'user']` — both project-level and user-level sources are loaded. + +To restrict a project to project-level resources only (e.g. CI, shared environments, or when `~/.claude/` contains personal commands you don't want surfacing in workflows): -Set in global or repo config: ```yaml assistants: claude: settingSources: - project - - user ``` -This is useful when you maintain coding style or identity preferences in `~/.claude/CLAUDE.md` and want Archon sessions to respect them. +Set in `~/.archon/config.yaml` (global) or `.archon/config.yaml` (repo-specific). ### Worktree file copying (`worktree.copyFiles`) @@ -223,7 +223,7 @@ Environment variables override all other configuration. They are organized by ca | Variable | Description | Default | | --- | --- | --- | -| `ARCHON_HOME` | Base directory for all Archon-managed files | `~/.archon` | +| `ARCHON_HOME` | Base directory for all Archon-managed files. **Ignored in Docker** — the container always uses `/.archon`. | `~/.archon` | | `PORT` | HTTP server listen port | `3090` (auto-allocated in worktrees) | | `LOG_LEVEL` | Logging verbosity (`fatal`, `error`, `warn`, `info`, `debug`, `trace`) | `info` | | `BOT_DISPLAY_NAME` | Bot name shown in batch-mode "starting" messages | `Archon` | @@ -323,7 +323,8 @@ When `CLAUDE_USE_GLOBAL_AUTH` is unset, Archon auto-detects: it uses explicit to | Variable | Description | Default | | --- | --- | --- | -| `ARCHON_DATA` | Host path for Archon data (workspaces, worktrees, artifacts) | Docker-managed volume | +| `ARCHON_DATA` | Host path for Archon data (workspaces, worktrees, artifacts). Compose-only — read by `docker-compose.yml` to choose the bind-mount source for `/.archon`; not read by Archon source code. | Docker-managed volume | +| `ARCHON_USER_HOME` | Host path for `/home/appuser` (Claude/Codex/Pi config, `~/.gitconfig`, shell history). Compose-only — read by `docker-compose.yml` to choose the bind-mount source for `/home/appuser`; not read by Archon source code. Persisted by default to a Docker-managed volume so user state survives rebuilds. | Docker-managed volume | | `DOMAIN` | Public domain for Caddy reverse proxy (TLS auto-provisioned) | -- | | `CADDY_BASIC_AUTH` | Caddy basicauth directive to protect Web UI and API | Disabled | | `AUTH_USERNAME` | Username for form-based auth (Caddy forward_auth) | -- | diff --git a/packages/providers/src/claude/provider.test.ts b/packages/providers/src/claude/provider.test.ts index 123d687989..c8b618d7ef 100644 --- a/packages/providers/src/claude/provider.test.ts +++ b/packages/providers/src/claude/provider.test.ts @@ -726,7 +726,7 @@ describe('ClaudeProvider', () => { expect(callArgs.options.settingSources).toEqual(['project', 'user']); }); - test('defaults settingSources to project when not provided', async () => { + test('defaults settingSources to project + user when not provided', async () => { mockQuery.mockImplementation(async function* () { yield { type: 'result', session_id: 'test-session' }; }); @@ -735,6 +735,26 @@ describe('ClaudeProvider', () => { // consume } + expect(mockQuery).toHaveBeenCalledTimes(1); + const callArgs = mockQuery.mock.calls[0][0] as { options: Record }; + expect(callArgs.options.settingSources).toEqual(['project', 'user']); + }); + + test("honors explicit settingSources: ['project'] to opt out of user scope", async () => { + // Locks in the contract: setting settingSources: ['project'] in + // .archon/config.yaml must NOT be silently widened to the new default. + // A future refactor that drops the `?? ['project', 'user']` guard would + // expand skill/command/agent scope for every project-only deployment. + mockQuery.mockImplementation(async function* () { + yield { type: 'result', session_id: 'test-session' }; + }); + + for await (const _ of client.sendQuery('test', '/tmp', undefined, { + assistantConfig: { settingSources: ['project'] }, + })) { + // consume + } + expect(mockQuery).toHaveBeenCalledTimes(1); const callArgs = mockQuery.mock.calls[0][0] as { options: Record }; expect(callArgs.options.settingSources).toEqual(['project']); diff --git a/packages/providers/src/claude/provider.ts b/packages/providers/src/claude/provider.ts index 1e55c00b93..5609156fad 100644 --- a/packages/providers/src/claude/provider.ts +++ b/packages/providers/src/claude/provider.ts @@ -622,7 +622,7 @@ function buildBaseClaudeOptions( permissionMode: 'bypassPermissions', allowDangerouslySkipPermissions: true, systemPrompt: requestOptions?.systemPrompt ?? { type: 'preset', preset: 'claude_code' }, - settingSources: assistantDefaults.settingSources ?? ['project'], + settingSources: assistantDefaults.settingSources ?? ['project', 'user'], hooks: buildToolCaptureHooks(toolResultQueue), stderr: (data: string): void => { const output = data.trim(); diff --git a/packages/providers/src/types.ts b/packages/providers/src/types.ts index fe47eff6c4..e259f86abd 100644 --- a/packages/providers/src/types.ts +++ b/packages/providers/src/types.ts @@ -9,8 +9,12 @@ export interface ClaudeProviderDefaults { [key: string]: unknown; model?: string; - /** Claude Code settingSources — controls which CLAUDE.md files are loaded. - * @default ['project'] + /** Claude Code settingSources — controls which sources the SDK loads: + * CLAUDE.md, skills, commands, agents, and hooks. Both project-level + * (`/.claude/`) and user-level (`~/.claude/`) are loaded by default. + * Set explicitly to `['project']` to scope a workflow to project-only + * resources (e.g. CI, shared environments). + * @default ['project', 'user'] */ settingSources?: ('project' | 'user')[]; /** Absolute path to the Claude Code SDK's `cli.js`. Required in compiled diff --git a/packages/server/src/scripts/setup-auth.ts b/packages/server/src/scripts/setup-auth.ts index 40031d5173..12a725c931 100644 --- a/packages/server/src/scripts/setup-auth.ts +++ b/packages/server/src/scripts/setup-auth.ts @@ -27,8 +27,25 @@ function setupAuth(): void { const refreshToken = process.env.CODEX_REFRESH_TOKEN; const accountId = process.env.CODEX_ACCOUNT_ID; - // Skip if Codex credentials not provided + // No CODEX_* env vars provided: warn if a persisted auth.json already + // exists on the volume (may be stale), otherwise skip with "unavailable". if (!idToken || !accessToken || !refreshToken || !accountId) { + // /home/appuser is now persisted across restarts in Docker, so a stale + // auth.json from a previous run with creds is not automatically wiped. + // Surface this so operators don't end up with Codex silently using old/revoked tokens. + const persistedAuthPath = path.join(os.homedir(), '.codex', 'auth.json'); + if (fs.existsSync(persistedAuthPath)) { + console.warn( + `⚠️ CODEX_* env vars not set, but persisted ${persistedAuthPath} exists from a previous run` + ); + console.warn( + ' Codex will attempt to use those credentials. If they are stale or revoked,' + ); + console.warn( + ' delete the file inside the container or wipe the archon_user_home volume to reset.' + ); + return; + } console.log('⏭️ Skipping Codex auth setup - credentials not provided'); console.log(' Codex assistant will be unavailable'); return;