Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cwd>/.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.
Expand Down
2 changes: 2 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,4 +47,5 @@ services:

volumes:
archon_data:
archon_user_home:
# postgres_data:
5 changes: 5 additions & 0 deletions docker-compose.override.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -122,6 +130,7 @@ services:

volumes:
archon_data:
archon_user_home:
postgres_data:
caddy_data:
caddy_config:
Expand Down
26 changes: 25 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,45 @@ 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),
# so git prints "dubious ownership" and refuses all operations.
# 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
Expand Down
38 changes: 38 additions & 0 deletions packages/docs-web/src/content/docs/deployment/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cwd>/.claude/` and your personal `~/.claude/`. Set it to `['project']` if you want to scope a workflow to project-only resources.

### Set as Default (Optional)

Expand Down
7 changes: 5 additions & 2 deletions packages/docs-web/src/content/docs/guides/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +134 to +135
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify that this setting is assistant-wide, not per-workflow.

“Scope a workflow” reads like node-level or workflow-level behavior, but settingSources is configured globally under assistants.claude. A small wording change here would keep the guide precise.

♻️ Suggested wording tweak
-Set `assistants.claude.settingSources: ['project']` in `.archon/config.yaml`
-to scope a workflow to project-level skills only.
+Set `assistants.claude.settingSources: ['project']` in `.archon/config.yaml`
+to keep Claude loading project-level resources only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs-web/src/content/docs/guides/skills.md` around lines 134 - 135,
The sentence is misleading: callers might think Set
`assistants.claude.settingSources: ['project']` in `.archon/config.yaml` scopes
an individual workflow, but `assistants.claude.settingSources` is an
assistant-wide configuration. Update the copy to state that
`assistants.claude.settingSources` in `.archon/config.yaml` applies to the
entire assistant (not per-workflow or per-node) and that using `['project']`
restricts all workflows under that assistant to project-level skills.


Skills installed via `npx skills add` land in `.claude/skills/` by default.
Use `-g` for global installation to `~/.claude/skills/`.

Expand Down
25 changes: 13 additions & 12 deletions packages/docs-web/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cwd>/.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)
Expand Down Expand Up @@ -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 `<cwd>/.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`)

Expand Down Expand Up @@ -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` |
Expand Down Expand Up @@ -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) | -- |
Expand Down
22 changes: 21 additions & 1 deletion packages/providers/src/claude/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
});
Expand All @@ -735,6 +735,26 @@ describe('ClaudeProvider', () => {
// consume
}

expect(mockQuery).toHaveBeenCalledTimes(1);
const callArgs = mockQuery.mock.calls[0][0] as { options: Record<string, unknown> };
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<string, unknown> };
expect(callArgs.options.settingSources).toEqual(['project']);
Expand Down
2 changes: 1 addition & 1 deletion packages/providers/src/claude/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 6 additions & 2 deletions packages/providers/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* (`<cwd>/.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
Expand Down
Loading
Loading