mirror of
https://github.com/coleam00/Archon
synced 2026-04-21 13:37:41 +00:00
fix(providers): replace Claude SDK embed with explicit binary-path resolver (#1217)
* feat(providers): replace Claude SDK embed with explicit binary-path resolver Drop `@anthropic-ai/claude-agent-sdk/embed` and resolve Claude Code via CLAUDE_BIN_PATH env → assistants.claude.claudeBinaryPath config → throw with install instructions. The embed's silent failure modes on macOS (#1210) and Windows (#1087) become actionable errors with a documented recovery path. Dev mode (bun run) remains auto-resolved via node_modules. The setup wizard auto-detects Claude Code by probing the native installer path (~/.local/bin/claude), npm global cli.js, and PATH, then writes CLAUDE_BIN_PATH to ~/.archon/.env. Dockerfile pre-sets CLAUDE_BIN_PATH so extenders using the compiled binary keep working. Release workflow gets negative and positive resolver smoke tests. Docs, CHANGELOG, README, .env.example, CLAUDE.md, test-release and archon skills all updated to reflect the curl-first install story. Retires #1210, #1087, #1091 (never merged, now obsolete). Implements #1176. * fix(providers): only pass --no-env-file when spawning Claude via Bun/Node `--no-env-file` is a Bun flag that prevents Bun from auto-loading `.env` from the subprocess cwd. It is only meaningful when the Claude Code executable is a `cli.js` file — in which case the SDK spawns it via `bun`/`node` and the flag reaches the runtime. When `CLAUDE_BIN_PATH` points at a native compiled Claude binary (e.g. `~/.local/bin/claude` from the curl installer, which is Anthropic's recommended default), the SDK executes the binary directly. Passing `--no-env-file` then goes straight to the native binary, which rejects it with `error: unknown option '--no-env-file'` and the subprocess exits code 1. Emit `executableArgs` only when the target is a `.js` file (dev mode or explicit cli.js path). Caught by end-to-end smoke testing against the curl-installed native Claude binary. * docs: record env-leak validation result in provider comment Verified end-to-end with sentinel `.env` and `.env.local` files in a workflow CWD that the native Claude binary (curl installer) does not auto-load `.env` files. With Archon's full spawn pathway and parent env stripped, the subprocess saw both sentinels as UNSET. The first-layer protection in `@archon/paths` (#1067) handles the inheritance leak; `--no-env-file` only matters for the Bun-spawned cli.js path, where it is still emitted. * chore(providers): cleanup pass — exports, docs, troubleshooting Final-sweep cleanup tied to the binary-resolver PR: - Mirror Codex's package surface for the new Claude resolver: add `./claude/binary-resolver` subpath export and re-export `resolveClaudeBinaryPath` + `claudeFileExists` from the package index. Renames the previously single `fileExists` re-export to `codexFileExists` for symmetry; nothing outside the providers package was importing it. - Add a "Claude Code not found" entry to the troubleshooting reference doc with platform-specific install snippets and pointers to the AI Assistants binary-path section. - Reframe the example claudeBinaryPath in reference/configuration.md away from cli.js-only language; it accepts either the native binary or cli.js. * test+refactor(providers, cli): address PR review feedback Two test gaps and one doc nit from the PR review (#1217): - Extract the `--no-env-file` decision into a pure exported helper `shouldPassNoEnvFile(cliPath)` so the native-binary branch is unit testable without mocking `BUNDLED_IS_BINARY` or running the full sendQuery pathway. Six new tests cover undefined, cli.js, native binary (Linux + Windows), Homebrew symlink, and suffix-only matching. Also adds a `claude.subprocess_env_file_flag` debug log so the security-adjacent decision is auditable. - Extract the three install-location probes in setup.ts into exported wrappers (`probeFileExists`, `probeNpmRoot`, `probeWhichClaude`) and export `detectClaudeExecutablePath` itself, so the probe order can be spied on. Six new tests cover each tier winning, fall-through ordering, npm-tier skip when not installed, and the which-resolved-but-stale-path edge case. - CLAUDE.md `claudeBinaryPath` placeholder updated to reflect that the field accepts either the native binary or cli.js (the example value was previously `/absolute/path/to/cli.js`, slightly misleading now that the curl-installer native binary is the default). Skipped from the review by deliberate scope decision: - `resolveClaudeBinaryPath` async-with-no-await: matches Codex's resolver signature exactly. Changing only Claude breaks symmetry; if pursued, do both providers in a separate cleanup PR. - `isAbsolute()` validation in parseClaudeConfig: Codex doesn't do it either. Resolver throws on non-existence already. - Atomic `.env` writes in setup wizard: pre-existing pattern this PR touched only adjacently. File as separate issue if needed. - classifyError branch in dag-executor for setup errors: scope creep. - `.env.example` "missing #" claim: false positive (verified all CLAUDE_BIN_PATH lines have proper comment prefixes). * fix(test): use path.join in Windows-compatible probe-order test The "tier 2 wins (npm cli.js)" test hardcoded forward-slash path comparisons, but `path.join` produces backslashes on Windows. Caused the Windows CI leg of the test suite to fail while macOS and Linux passed. Use `path.join` for both the mock return value and the expectation so the separator matches whatever the platform produces.
This commit is contained in:
parent
33d31c44f1
commit
81859d6842
28 changed files with 946 additions and 19 deletions
|
|
@ -119,6 +119,8 @@ If Bun was just installed in Prerequisites (macOS/Linux), use `~/.bun/bin/bun` i
|
|||
3. Verify: `archon version`
|
||||
4. Check Claude is installed: `which claude`, then `claude /login` if needed
|
||||
|
||||
> **Note — Claude Code binary path.** Archon does not bundle Claude Code. In compiled Archon binaries (quick install, Homebrew), the Claude Code SDK needs `CLAUDE_BIN_PATH` set to the absolute path of its `cli.js`. The `archon setup` wizard in Step 4 auto-detects this via `npm root -g` and writes it to `~/.archon/.env` — no manual action needed in the typical case. Source installs (`bun run`) don't need this; the SDK finds `cli.js` via `node_modules` automatically.
|
||||
|
||||
## Step 4: Configure Credentials
|
||||
|
||||
The CLI loads infrastructure config (database, tokens) from `~/.archon/.env` only. This prevents conflicts with project `.env` files that may contain different database URLs.
|
||||
|
|
|
|||
|
|
@ -222,7 +222,23 @@ git commit -q --allow-empty -m init
|
|||
|
||||
### Test 3 — SDK path works (assist workflow)
|
||||
|
||||
In the same `$TESTREPO`:
|
||||
**Prerequisite.** Compiled binaries require Claude Code installed on the host and a configured binary path. Before running this test, ensure one of:
|
||||
|
||||
```bash
|
||||
# Option A — env var (easy for ad-hoc testing)
|
||||
# After the native installer (Anthropic's default):
|
||||
export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"
|
||||
# Or after npm global install:
|
||||
export CLAUDE_BIN_PATH="$(npm root -g)/@anthropic-ai/claude-code/cli.js"
|
||||
|
||||
# Option B — config file (persistent)
|
||||
# Add to ~/.archon/config.yaml:
|
||||
# assistants:
|
||||
# claude:
|
||||
# claudeBinaryPath: /absolute/path/to/claude
|
||||
```
|
||||
|
||||
Then in the same `$TESTREPO`:
|
||||
|
||||
```bash
|
||||
"$BINARY" workflow run assist "say hello and nothing else" 2>&1 | tee /tmp/archon-test-assist.log
|
||||
|
|
@ -232,15 +248,34 @@ In the same `$TESTREPO`:
|
|||
|
||||
- Exit code 0
|
||||
- The Claude subprocess spawns successfully (no `spawn EACCES`, `ENOENT`, or `process exited with code 1` in the early output)
|
||||
- No `Claude Code CLI not found` error (that means the resolver rejected the configured path — verify the cli.js actually exists)
|
||||
- A response is produced (any response — even just "hello" — proves the SDK round-trip works)
|
||||
|
||||
**Common failures:**
|
||||
|
||||
- `Claude Code not found` → `CLAUDE_BIN_PATH` / `claudeBinaryPath` is unset or points at a non-existent file. Fix the path and re-run.
|
||||
- `Module not found "/Users/runner/..."` → regression of #1210: the resolver was bypassed and the SDK's `import.meta.url` fallback leaked a build-host path. Investigate `packages/providers/src/claude/provider.ts` and the resolver.
|
||||
- `Credit balance is too low` → auth is pointing at an exhausted API key (check `CLAUDE_USE_GLOBAL_AUTH` and `~/.archon/.env`)
|
||||
- `unable to determine transport target for "pino-pretty"` → #960 regression, binary crashes on TTY
|
||||
- `package.json not found (bad installation?)` → #961 regression, `isBinaryBuild` detection broken
|
||||
- Process exits before producing output → generic spawn failure, capture stderr
|
||||
|
||||
### Test 3b — Resolver error path (run without `CLAUDE_BIN_PATH`)
|
||||
|
||||
Quickly verify the resolver fails loud when nothing is configured:
|
||||
|
||||
```bash
|
||||
(unset CLAUDE_BIN_PATH; "$BINARY" workflow run assist "hello" 2>&1 | tee /tmp/archon-test-no-path.log)
|
||||
```
|
||||
|
||||
**Pass criteria (when no `~/.archon/config.yaml` configures `claudeBinaryPath`):**
|
||||
|
||||
- Error message contains `Claude Code not found`
|
||||
- Error message mentions both `CLAUDE_BIN_PATH` and `claudeBinaryPath` as remediation options
|
||||
- No `Module not found` stack traces referencing the CI filesystem
|
||||
|
||||
If you *do* have `claudeBinaryPath` set globally, skip this test or temporarily rename `~/.archon/config.yaml`.
|
||||
|
||||
### Test 4 — Env-leak gate refuses a leaky .env (optional, for releases including #1036/#1038/#983)
|
||||
|
||||
Create a second throwaway repo with a fake sensitive key:
|
||||
|
|
|
|||
14
.env.example
14
.env.example
|
|
@ -14,6 +14,20 @@ CLAUDE_USE_GLOBAL_AUTH=true
|
|||
# CLAUDE_CODE_OAUTH_TOKEN=...
|
||||
# CLAUDE_API_KEY=...
|
||||
|
||||
# Claude Code executable path (REQUIRED for compiled Archon binaries)
|
||||
# Archon does not bundle Claude Code — install it separately and point us at it.
|
||||
# Dev mode (`bun run`) auto-resolves via node_modules.
|
||||
# Alternatively, set `assistants.claude.claudeBinaryPath` in ~/.archon/config.yaml.
|
||||
#
|
||||
# Install (Anthropic's recommended native installer):
|
||||
# macOS/Linux: curl -fsSL https://claude.ai/install.sh | bash
|
||||
# Windows: irm https://claude.ai/install.ps1 | iex
|
||||
#
|
||||
# Then:
|
||||
# CLAUDE_BIN_PATH=$HOME/.local/bin/claude (native installer)
|
||||
# CLAUDE_BIN_PATH=$(npm root -g)/@anthropic-ai/claude-code/cli.js (npm alternative)
|
||||
# CLAUDE_BIN_PATH=
|
||||
|
||||
# Codex Authentication (get from ~/.codex/auth.json after running 'codex login')
|
||||
# Required if using Codex as AI assistant
|
||||
# On Linux/Mac: cat ~/.codex/auth.json
|
||||
|
|
|
|||
77
.github/workflows/release.yml
vendored
77
.github/workflows/release.yml
vendored
|
|
@ -124,6 +124,83 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
- name: Smoke-test Claude binary-path resolver (negative case)
|
||||
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
|
||||
run: |
|
||||
# With no CLAUDE_BIN_PATH and no config, running a Claude workflow must
|
||||
# fail with a clear, user-facing error — NOT with "Module not found
|
||||
# /Users/runner/..." which would indicate the resolver was bypassed.
|
||||
BIN="$PWD/dist/${{ matrix.binary }}"
|
||||
TMP_REPO=$(mktemp -d)
|
||||
cd "$TMP_REPO"
|
||||
git init -q
|
||||
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init
|
||||
|
||||
# Run without CLAUDE_BIN_PATH set. Expect a clean resolver error.
|
||||
# Capture both stdout and stderr; we only care that the resolver message is present.
|
||||
set +e
|
||||
OUTPUT=$(env -u CLAUDE_BIN_PATH "$BIN" workflow run archon-assist "hello" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
echo "$OUTPUT"
|
||||
|
||||
if echo "$OUTPUT" | grep -qE 'Module not found.*Users/runner'; then
|
||||
echo "::error::Resolver was bypassed — SDK hit the import.meta.url fallback (regression of #1210)"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -q "Claude Code not found"; then
|
||||
echo "::error::Expected 'Claude Code not found' error when CLAUDE_BIN_PATH is unset"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -q "CLAUDE_BIN_PATH"; then
|
||||
echo "::error::Error message does not reference CLAUDE_BIN_PATH remediation"
|
||||
exit 1
|
||||
fi
|
||||
echo "::notice::Resolver error path works (exit code: $EXIT_CODE)"
|
||||
|
||||
- name: Smoke-test Claude subprocess spawn (positive case)
|
||||
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
|
||||
run: |
|
||||
# Install Claude Code via the native installer (Anthropic's recommended
|
||||
# default) and run a workflow with CLAUDE_BIN_PATH set. The subprocess
|
||||
# must spawn cleanly. We do NOT require the query to succeed (no auth
|
||||
# in CI — an auth error is fine and expected); we only fail if the SDK
|
||||
# can't find the executable, which would indicate a resolver regression.
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
CLI_PATH="$HOME/.local/bin/claude"
|
||||
if [ ! -x "$CLI_PATH" ]; then
|
||||
echo "::error::Claude Code binary not found after curl install at $CLI_PATH"
|
||||
ls -la "$HOME/.local/bin/" || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Using CLAUDE_BIN_PATH=$CLI_PATH"
|
||||
|
||||
BIN="$PWD/dist/${{ matrix.binary }}"
|
||||
TMP_REPO=$(mktemp -d)
|
||||
cd "$TMP_REPO"
|
||||
git init -q
|
||||
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init
|
||||
|
||||
set +e
|
||||
OUTPUT=$(CLAUDE_BIN_PATH="$CLI_PATH" "$BIN" workflow run archon-assist "hello" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
echo "$OUTPUT"
|
||||
|
||||
if echo "$OUTPUT" | grep -qE 'Module not found.*(cli\.js|Users/runner)'; then
|
||||
echo "::error::Subprocess could not find the executable (resolver regression)"
|
||||
exit 1
|
||||
fi
|
||||
if echo "$OUTPUT" | grep -q "Claude Code not found"; then
|
||||
echo "::error::Resolver failed even though CLAUDE_BIN_PATH was set to an existing file"
|
||||
exit 1
|
||||
fi
|
||||
# Any of these outcomes are acceptable — they prove the subprocess spawned:
|
||||
# - auth error ("credit balance", "unauthorized", "authentication")
|
||||
# - rate-limit / API error
|
||||
# - successful query (if auth was injected via some other mechanism)
|
||||
echo "::notice::Claude subprocess spawn path is healthy (exit code: $EXIT_CODE)"
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
|
|||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Claude Code binary resolution** (breaking for compiled binary users): Archon no longer embeds the Claude Code SDK into compiled binaries. In compiled builds, you must install Claude Code separately (`curl -fsSL https://claude.ai/install.sh | bash` on macOS/Linux, `irm https://claude.ai/install.ps1 | iex` on Windows, or `npm install -g @anthropic-ai/claude-code`) and point Archon at the executable via `CLAUDE_BIN_PATH` env var or `assistants.claude.claudeBinaryPath` in `.archon/config.yaml`. The Claude Agent SDK accepts either the native compiled binary (from the curl/PowerShell installer at `~/.local/bin/claude`) or a JS `cli.js` (from the npm install). Dev mode (`bun run`) is unaffected — the SDK resolves via `node_modules` as before. The Docker image ships Claude Code pre-installed with `CLAUDE_BIN_PATH` pre-set, so `docker run` still works out of the box. Resolves silent "Module not found /Users/runner/..." failures on macOS (#1210) and Windows (#1087).
|
||||
|
||||
### Added
|
||||
|
||||
- **`CLAUDE_BIN_PATH` environment variable** — highest-precedence override for the Claude Code SDK `cli.js` path (#1176)
|
||||
- **`assistants.claude.claudeBinaryPath` config option** — durable config-file alternative to the env var (#1176)
|
||||
- **Release-workflow Claude subprocess smoke test** — the release CI now installs Claude Code on the Linux runner and exercises the resolver + subprocess spawn, catching binary-resolution regressions before they ship
|
||||
|
||||
### Removed
|
||||
|
||||
- **`@anthropic-ai/claude-agent-sdk/embed` import** — the Bun `with { type: 'file' }` asset-embedding path and its `$bunfs` extraction logic. The embed was a bundler-dependent optimization that failed silently when Bun couldn't produce a usable virtual FS path (#1210, #1087); it is replaced by explicit binary-path resolution.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Cross-clone worktree isolation**: prevent workflows in one local clone from silently adopting worktrees or DB state owned by another local clone of the same remote. Two clones sharing a remote previously resolved to the same `codebase_id`, causing the isolation resolver's DB-driven paths (`findReusable`, `findLinkedIssueEnv`, `tryBranchAdoption`) to return the other clone's environment. All adoption paths now verify the worktree's `.git` pointer matches the requesting clone and throw a classified error on mismatch. `archon-implement` prompt was also tightened to stop AI agents from adopting unrelated branches they see via `git branch`. Thanks to @halindrome for the three-issue root-cause mapping. (#1193, #1188, #1183, #1198, #1206)
|
||||
|
|
|
|||
|
|
@ -468,6 +468,11 @@ assistants:
|
|||
settingSources: # Controls which CLAUDE.md files Claude SDK loads
|
||||
- project # Default: only project-level CLAUDE.md
|
||||
- user # Optional: also load ~/.claude/CLAUDE.md
|
||||
claudeBinaryPath: /absolute/path/to/claude # Optional: Claude Code executable.
|
||||
# Native binary (curl installer at
|
||||
# ~/.local/bin/claude) or npm cli.js.
|
||||
# Required in compiled binaries if
|
||||
# CLAUDE_BIN_PATH env var is not set.
|
||||
codex:
|
||||
model: gpt-5.3-codex
|
||||
modelReasoningEffort: medium # 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm \
|
|||
# Point agent-browser to system Chromium (avoids ~400MB Chrome for Testing download)
|
||||
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
# Pre-configure the Claude Code SDK cli.js path for any consumer that runs
|
||||
# a compiled Archon binary inside (or extending) this image. In source mode
|
||||
# (the default `bun run start` ENTRYPOINT), BUNDLED_IS_BINARY is false and
|
||||
# this variable is ignored — the SDK resolves cli.js via node_modules. Kept
|
||||
# here so extenders don't need to rediscover the path.
|
||||
# Path matches the hoisted layout produced by `bun install --linker=hoisted`.
|
||||
ENV CLAUDE_BIN_PATH=/app/node_modules/@anthropic-ai/claude-agent-sdk/cli.js
|
||||
|
||||
# Create non-root user for running Claude Code
|
||||
# Claude Code refuses to run with --dangerously-skip-permissions as root for security
|
||||
RUN useradd -m -u 1001 -s /bin/bash appuser \
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -171,6 +171,22 @@ irm https://archon.diy/install.ps1 | iex
|
|||
brew install coleam00/archon/archon
|
||||
```
|
||||
|
||||
> **Compiled binaries need a `CLAUDE_BIN_PATH`.** The quick-install binaries
|
||||
> don't bundle Claude Code. Install it separately, then point Archon at it:
|
||||
>
|
||||
> ```bash
|
||||
> # macOS / Linux / WSL
|
||||
> curl -fsSL https://claude.ai/install.sh | bash
|
||||
> export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"
|
||||
>
|
||||
> # Windows (PowerShell)
|
||||
> irm https://claude.ai/install.ps1 | iex
|
||||
> $env:CLAUDE_BIN_PATH = "$env:USERPROFILE\.local\bin\claude.exe"
|
||||
> ```
|
||||
>
|
||||
> Or set `assistants.claude.claudeBinaryPath` in `~/.archon/config.yaml`.
|
||||
> The Docker image ships Claude Code pre-installed. See [AI Assistants → Binary path configuration](https://archon.diy/docs/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only) for details.
|
||||
|
||||
### Start Using Archon
|
||||
|
||||
Once you've completed either setup path, go to your project and start working:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import {
|
|||
generateWebhookSecret,
|
||||
spawnTerminalWithSetup,
|
||||
copyArchonSkill,
|
||||
detectClaudeExecutablePath,
|
||||
} from './setup';
|
||||
import * as setupModule from './setup';
|
||||
|
||||
// Test directory for file operations
|
||||
const TEST_DIR = join(tmpdir(), 'archon-setup-test-' + Date.now());
|
||||
|
|
@ -176,6 +178,41 @@ CODEX_ACCOUNT_ID=account1
|
|||
expect(content).toContain('CLAUDE_API_KEY=sk-test-key');
|
||||
});
|
||||
|
||||
it('emits CLAUDE_BIN_PATH when claudeBinaryPath is configured', () => {
|
||||
const content = generateEnvContent({
|
||||
database: { type: 'sqlite' },
|
||||
ai: {
|
||||
claude: true,
|
||||
claudeAuthType: 'global',
|
||||
claudeBinaryPath: '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
||||
codex: false,
|
||||
defaultAssistant: 'claude',
|
||||
},
|
||||
platforms: { github: false, telegram: false, slack: false, discord: false },
|
||||
botDisplayName: 'Archon',
|
||||
});
|
||||
|
||||
expect(content).toContain(
|
||||
'CLAUDE_BIN_PATH=/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js'
|
||||
);
|
||||
});
|
||||
|
||||
it('omits CLAUDE_BIN_PATH when not configured', () => {
|
||||
const content = generateEnvContent({
|
||||
database: { type: 'sqlite' },
|
||||
ai: {
|
||||
claude: true,
|
||||
claudeAuthType: 'global',
|
||||
codex: false,
|
||||
defaultAssistant: 'claude',
|
||||
},
|
||||
platforms: { github: false, telegram: false, slack: false, discord: false },
|
||||
botDisplayName: 'Archon',
|
||||
});
|
||||
|
||||
expect(content).not.toContain('CLAUDE_BIN_PATH=');
|
||||
});
|
||||
|
||||
it('should include platform configurations', () => {
|
||||
const content = generateEnvContent({
|
||||
database: { type: 'sqlite' },
|
||||
|
|
@ -418,3 +455,82 @@ CODEX_ACCOUNT_ID=account1
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectClaudeExecutablePath probe order', () => {
|
||||
// Use spies on the exported probe wrappers so each tier can be controlled
|
||||
// independently without touching the real filesystem or shell.
|
||||
let fileExistsSpy: ReturnType<typeof spyOn>;
|
||||
let npmRootSpy: ReturnType<typeof spyOn>;
|
||||
let whichSpy: ReturnType<typeof spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
fileExistsSpy = spyOn(setupModule, 'probeFileExists').mockReturnValue(false);
|
||||
npmRootSpy = spyOn(setupModule, 'probeNpmRoot').mockReturnValue(null);
|
||||
whichSpy = spyOn(setupModule, 'probeWhichClaude').mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fileExistsSpy.mockRestore();
|
||||
npmRootSpy.mockRestore();
|
||||
whichSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('returns the native installer path when present (tier 1 wins)', () => {
|
||||
// Native path exists; subsequent probes must not be called.
|
||||
fileExistsSpy.mockImplementation(
|
||||
(p: string) => p.includes('.local/bin/claude') || p.includes('.local\\bin\\claude')
|
||||
);
|
||||
const result = detectClaudeExecutablePath();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/\.local[\\/]bin[\\/]claude/);
|
||||
// Tier 2 / 3 must not have been consulted.
|
||||
expect(npmRootSpy).not.toHaveBeenCalled();
|
||||
expect(whichSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls through to npm cli.js when native is missing (tier 2 wins)', () => {
|
||||
// Use path.join so the expected result matches whatever separator the
|
||||
// production code produces on the current platform (backslash on Windows,
|
||||
// forward slash elsewhere).
|
||||
const npmRoot = join('fake', 'npm', 'root');
|
||||
const expectedCliJs = join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
|
||||
npmRootSpy.mockReturnValue(npmRoot);
|
||||
fileExistsSpy.mockImplementation((p: string) => p === expectedCliJs);
|
||||
const result = detectClaudeExecutablePath();
|
||||
expect(result).toBe(expectedCliJs);
|
||||
// Tier 3 must not have been consulted.
|
||||
expect(whichSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls through to which/where when native and npm probes both miss (tier 3 wins)', () => {
|
||||
npmRootSpy.mockReturnValue('/fake/npm/root');
|
||||
// Native miss, npm cli.js miss, but `which claude` returns a path that exists.
|
||||
whichSpy.mockReturnValue('/opt/homebrew/bin/claude');
|
||||
fileExistsSpy.mockImplementation((p: string) => p === '/opt/homebrew/bin/claude');
|
||||
const result = detectClaudeExecutablePath();
|
||||
expect(result).toBe('/opt/homebrew/bin/claude');
|
||||
});
|
||||
|
||||
it('returns null when every probe misses', () => {
|
||||
// All defaults already return false/null; nothing to override.
|
||||
expect(detectClaudeExecutablePath()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not return a which-resolved path that fails the existsSync check', () => {
|
||||
// `which` returns a path string but the file is not actually present
|
||||
// (stale PATH entry, dangling symlink, etc.) — must not be returned.
|
||||
npmRootSpy.mockReturnValue('/fake/npm/root');
|
||||
whichSpy.mockReturnValue('/stale/path/claude');
|
||||
fileExistsSpy.mockReturnValue(false);
|
||||
expect(detectClaudeExecutablePath()).toBeNull();
|
||||
});
|
||||
|
||||
it('skips npm tier when probeNpmRoot returns null (e.g. npm not installed)', () => {
|
||||
// npm probe fails; tier 3 must still run.
|
||||
whichSpy.mockReturnValue('/usr/local/bin/claude');
|
||||
fileExistsSpy.mockImplementation((p: string) => p === '/usr/local/bin/claude');
|
||||
const result = detectClaudeExecutablePath();
|
||||
expect(result).toBe('/usr/local/bin/claude');
|
||||
expect(npmRootSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ interface SetupConfig {
|
|||
claudeAuthType?: 'global' | 'apiKey' | 'oauthToken';
|
||||
claudeApiKey?: string;
|
||||
claudeOauthToken?: string;
|
||||
/** Absolute path to Claude Code SDK's cli.js. Written as CLAUDE_BIN_PATH
|
||||
* in ~/.archon/.env. Required in compiled Archon binaries; harmless in dev. */
|
||||
claudeBinaryPath?: string;
|
||||
codex: boolean;
|
||||
codexTokens?: CodexTokens;
|
||||
defaultAssistant: string;
|
||||
|
|
@ -160,6 +163,85 @@ function isCommandAvailable(command: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe wrappers — exported so tests can spy on each tier independently.
|
||||
* Direct imports of `existsSync` and `execSync` cannot be intercepted by
|
||||
* `spyOn` (esm rebinding limitation), so we route the probes through these
|
||||
* thin wrappers and let the test mock them in isolation.
|
||||
*/
|
||||
export function probeFileExists(path: string): boolean {
|
||||
return existsSync(path);
|
||||
}
|
||||
|
||||
export function probeNpmRoot(): string | null {
|
||||
try {
|
||||
const out = execSync('npm root -g', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
return out || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function probeWhichClaude(): string | null {
|
||||
try {
|
||||
const checkCmd = process.platform === 'win32' ? 'where' : 'which';
|
||||
const resolved = execSync(`${checkCmd} claude`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
// On Windows, `where` can return multiple lines — take the first.
|
||||
const first = resolved.split(/\r?\n/)[0]?.trim();
|
||||
return first ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to locate the Claude Code executable on disk.
|
||||
*
|
||||
* Compiled Archon binaries need an explicit path because the Claude Agent
|
||||
* SDK's `import.meta.url` resolution is frozen to the build host's filesystem.
|
||||
* The SDK's `pathToClaudeCodeExecutable` accepts either:
|
||||
* - A native compiled binary (from the curl/PowerShell/winget installers — current default)
|
||||
* - A JS `cli.js` (from `npm install -g @anthropic-ai/claude-code` — older path)
|
||||
*
|
||||
* We probe the well-known install locations in order:
|
||||
* 1. Native installer (`~/.local/bin/claude` on macOS/Linux, `%USERPROFILE%\.local\bin\claude.exe` on Windows)
|
||||
* 2. npm global `cli.js`
|
||||
* 3. `which claude` / `where claude` — fallback if the user installed via Homebrew, winget, or a custom layout
|
||||
*
|
||||
* Returns null on total failure so the caller can prompt the user.
|
||||
* Detection is best-effort; the caller should let users override.
|
||||
*
|
||||
* Exported so the probe order can be tested directly by spying on the
|
||||
* tier wrappers above (`probeFileExists`, `probeNpmRoot`, `probeWhichClaude`).
|
||||
*/
|
||||
export function detectClaudeExecutablePath(): string | null {
|
||||
// 1. Native installer default location (primary Anthropic-recommended path)
|
||||
const nativePath =
|
||||
process.platform === 'win32'
|
||||
? join(homedir(), '.local', 'bin', 'claude.exe')
|
||||
: join(homedir(), '.local', 'bin', 'claude');
|
||||
if (probeFileExists(nativePath)) return nativePath;
|
||||
|
||||
// 2. npm global cli.js
|
||||
const npmRoot = probeNpmRoot();
|
||||
if (npmRoot) {
|
||||
const npmCliJs = join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
|
||||
if (probeFileExists(npmCliJs)) return npmCliJs;
|
||||
}
|
||||
|
||||
// 3. Fallback: resolve via `which` / `where` (Homebrew, winget, custom layouts)
|
||||
const fromPath = probeWhichClaude();
|
||||
if (fromPath && probeFileExists(fromPath)) return fromPath;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node.js version if installed, or null if not
|
||||
*/
|
||||
|
|
@ -210,7 +292,7 @@ After installation, run: claude /login`,
|
|||
Install using one of these methods:
|
||||
|
||||
Recommended for macOS (no Node.js required):
|
||||
brew install --cask codex
|
||||
brew install codex
|
||||
|
||||
Or via npm (requires Node.js 18+):
|
||||
npm install -g @openai/codex
|
||||
|
|
@ -353,6 +435,62 @@ function tryReadCodexAuth(): CodexTokens | null {
|
|||
/**
|
||||
* Collect Claude authentication method
|
||||
*/
|
||||
/**
|
||||
* Resolve the Claude Code executable path for CLAUDE_BIN_PATH.
|
||||
* Auto-detects common install locations and falls back to prompting the user.
|
||||
* Returns undefined if the user declines to configure (setup continues; the
|
||||
* compiled binary will error with clear instructions on first Claude query).
|
||||
*/
|
||||
async function collectClaudeBinaryPath(): Promise<string | undefined> {
|
||||
const detected = detectClaudeExecutablePath();
|
||||
|
||||
if (detected) {
|
||||
const useDetected = await confirm({
|
||||
message: `Found Claude Code at ${detected}. Write this to CLAUDE_BIN_PATH?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (isCancel(useDetected)) {
|
||||
cancel('Setup cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
if (useDetected) return detected;
|
||||
}
|
||||
|
||||
const nativeExample =
|
||||
process.platform === 'win32' ? '%USERPROFILE%\\.local\\bin\\claude.exe' : '~/.local/bin/claude';
|
||||
|
||||
note(
|
||||
'Compiled Archon binaries need CLAUDE_BIN_PATH set to the Claude Code executable.\n' +
|
||||
'In dev (`bun run`) this is ignored — the SDK resolves it via node_modules.\n\n' +
|
||||
'Recommended (Anthropic default — native installer):\n' +
|
||||
` macOS/Linux: ${nativeExample}\n` +
|
||||
' Windows: %USERPROFILE%\\.local\\bin\\claude.exe\n\n' +
|
||||
'Alternative (npm global install):\n' +
|
||||
' $(npm root -g)/@anthropic-ai/claude-code/cli.js',
|
||||
'Claude binary path'
|
||||
);
|
||||
|
||||
const customPath = await text({
|
||||
message: 'Absolute path to the Claude Code executable (leave blank to skip):',
|
||||
placeholder: nativeExample,
|
||||
});
|
||||
|
||||
if (isCancel(customPath)) {
|
||||
cancel('Setup cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const trimmed = (customPath ?? '').trim();
|
||||
if (!trimmed) return undefined;
|
||||
|
||||
if (!existsSync(trimmed)) {
|
||||
log.warning(
|
||||
`Path does not exist: ${trimmed}. Saving anyway — the compiled binary will error on first use until this is correct.`
|
||||
);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async function collectClaudeAuth(): Promise<{
|
||||
authType: 'global' | 'apiKey' | 'oauthToken';
|
||||
apiKey?: string;
|
||||
|
|
@ -662,6 +800,7 @@ After upgrading, run 'archon setup' again.`,
|
|||
let claudeAuthType: 'global' | 'apiKey' | 'oauthToken' | undefined;
|
||||
let claudeApiKey: string | undefined;
|
||||
let claudeOauthToken: string | undefined;
|
||||
let claudeBinaryPath: string | undefined;
|
||||
let codexTokens: CodexTokens | undefined;
|
||||
|
||||
// Collect Claude auth if selected
|
||||
|
|
@ -670,6 +809,7 @@ After upgrading, run 'archon setup' again.`,
|
|||
claudeAuthType = claudeAuth.authType;
|
||||
claudeApiKey = claudeAuth.apiKey;
|
||||
claudeOauthToken = claudeAuth.oauthToken;
|
||||
claudeBinaryPath = await collectClaudeBinaryPath();
|
||||
}
|
||||
|
||||
// Collect Codex auth if selected
|
||||
|
|
@ -710,6 +850,7 @@ After upgrading, run 'archon setup' again.`,
|
|||
claudeAuthType,
|
||||
claudeApiKey,
|
||||
claudeOauthToken,
|
||||
...(claudeBinaryPath !== undefined ? { claudeBinaryPath } : {}),
|
||||
codex: hasCodex,
|
||||
codexTokens,
|
||||
defaultAssistant,
|
||||
|
|
@ -1070,6 +1211,9 @@ export function generateEnvContent(config: SetupConfig): string {
|
|||
lines.push('CLAUDE_USE_GLOBAL_AUTH=false');
|
||||
lines.push(`CLAUDE_CODE_OAUTH_TOKEN=${config.ai.claudeOauthToken}`);
|
||||
}
|
||||
if (config.ai.claudeBinaryPath) {
|
||||
lines.push(`CLAUDE_BIN_PATH=${config.ai.claudeBinaryPath}`);
|
||||
}
|
||||
} else {
|
||||
lines.push('# Claude not configured');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ sidebar:
|
|||
|
||||
Deploy Archon on a server with Docker. Includes automatic HTTPS, PostgreSQL, and the Web UI.
|
||||
|
||||
> **Claude Code is pre-installed in the image.** The official `ghcr.io/coleam00/archon` image
|
||||
> ships with Claude Code installed via npm and `CLAUDE_BIN_PATH` pre-set — no extra configuration
|
||||
> required. If you build a custom image that omits the npm install, set `CLAUDE_BIN_PATH` yourself
|
||||
> to point at a mounted `cli.js` (see [AI Assistants → Binary path configuration](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only)).
|
||||
|
||||
---
|
||||
|
||||
## Cloud-Init (Fastest Setup)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ Local development with SQLite is the recommended default. No database setup is n
|
|||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh) 1.0+
|
||||
- At least one AI assistant configured (Claude Code or Codex)
|
||||
- At least one AI assistant installed and configured (Claude Code or Codex — Archon orchestrates them, it does not bundle them)
|
||||
- A GitHub token for repository cloning (`GH_TOKEN` / `GITHUB_TOKEN`)
|
||||
|
||||
> Source installs (`bun run`) auto-resolve Claude Code's `cli.js` via `node_modules`. Compiled Archon binaries require `CLAUDE_BIN_PATH` or `assistants.claude.claudeBinaryPath` — see [AI Assistants → Binary path configuration](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only).
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -15,6 +15,64 @@ You must configure **at least one** AI assistant. Both can be configured if desi
|
|||
|
||||
**Recommended for Claude Pro/Max subscribers.**
|
||||
|
||||
Archon does not bundle Claude Code. Install it separately, then in compiled Archon binaries, point Archon at the executable. In dev (`bun run`), Archon finds it automatically via `node_modules`.
|
||||
|
||||
### Install Claude Code
|
||||
|
||||
Anthropic's native installer is the primary recommended install path:
|
||||
|
||||
**macOS / Linux / WSL:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
irm https://claude.ai/install.ps1 | iex
|
||||
```
|
||||
|
||||
**Alternatives:**
|
||||
|
||||
- macOS via Homebrew: `brew install --cask claude-code`
|
||||
- npm (any platform): `npm install -g @anthropic-ai/claude-code`
|
||||
- Windows via winget: `winget install Anthropic.ClaudeCode`
|
||||
|
||||
See [Anthropic's setup guide](https://code.claude.com/docs/en/setup) for the full list and auto-update caveats per install path.
|
||||
|
||||
### Binary path configuration (compiled binaries only)
|
||||
|
||||
Compiled Archon binaries cannot auto-discover Claude Code at runtime. Supply the path via either:
|
||||
|
||||
1. **Environment variable** (highest precedence):
|
||||
```ini
|
||||
CLAUDE_BIN_PATH=/absolute/path/to/claude
|
||||
```
|
||||
2. **Config file** (`~/.archon/config.yaml` or a repo-local `.archon/config.yaml`):
|
||||
```yaml
|
||||
assistants:
|
||||
claude:
|
||||
claudeBinaryPath: /absolute/path/to/claude
|
||||
```
|
||||
|
||||
If neither is set in a compiled binary, Archon throws with install instructions on first Claude query.
|
||||
|
||||
The Claude Agent SDK accepts either the native compiled binary or a JS `cli.js`.
|
||||
|
||||
**Typical paths by install method:**
|
||||
|
||||
| Install method | Typical executable path |
|
||||
|---|---|
|
||||
| Native curl installer (macOS/Linux) | `~/.local/bin/claude` |
|
||||
| Native PowerShell installer (Windows) | `%USERPROFILE%\.local\bin\claude.exe` |
|
||||
| Homebrew cask | `$(brew --prefix)/bin/claude` (symlink) |
|
||||
| npm global install | `$(npm root -g)/@anthropic-ai/claude-code/cli.js` |
|
||||
| Windows winget | Resolvable via `where claude` |
|
||||
| Docker (`ghcr.io/coleam00/archon`) | Pre-set via `ENV CLAUDE_BIN_PATH` in the image — no action required |
|
||||
|
||||
If in doubt, `which claude` (macOS/Linux) or `where claude` (Windows) will resolve the executable on your PATH after any of the installers above.
|
||||
|
||||
### Authentication Options
|
||||
|
||||
Claude Code supports three authentication modes via `CLAUDE_USE_GLOBAL_AUTH`:
|
||||
|
|
@ -62,6 +120,9 @@ assistants:
|
|||
settingSources:
|
||||
- project # Default: only project-level CLAUDE.md
|
||||
- user # Optional: also load ~/.claude/CLAUDE.md
|
||||
# Optional: absolute path to the Claude Code executable.
|
||||
# Required in compiled Archon binaries if CLAUDE_BIN_PATH is not set.
|
||||
# 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`.
|
||||
|
|
@ -76,10 +137,46 @@ DEFAULT_AI_ASSISTANT=claude
|
|||
|
||||
## Codex
|
||||
|
||||
### Authenticate with Codex CLI
|
||||
Archon does not bundle the Codex CLI. Install it, then authenticate.
|
||||
|
||||
### Install the Codex CLI
|
||||
|
||||
```bash
|
||||
# Any platform (primary method):
|
||||
npm install -g @openai/codex
|
||||
|
||||
# macOS alternative:
|
||||
brew install codex
|
||||
|
||||
# Windows: npm install works but is experimental.
|
||||
# OpenAI recommends WSL2 for the best experience.
|
||||
```
|
||||
|
||||
Native prebuilt binaries (`.dmg`, `.tar.gz`, `.exe`) are also published on the [Codex releases page](https://github.com/openai/codex/releases) for users who prefer a direct binary — drop one in `~/.archon/vendor/codex/codex` (or `codex.exe` on Windows) and Archon will find it automatically in compiled binary mode.
|
||||
|
||||
See [OpenAI's Codex CLI docs](https://developers.openai.com/codex/cli) for the full install matrix.
|
||||
|
||||
### Binary path configuration (compiled binaries only)
|
||||
|
||||
In compiled Archon binaries, if `codex` is not on the default PATH Archon expects, supply the path via either:
|
||||
|
||||
1. **Environment variable** (highest precedence):
|
||||
```ini
|
||||
CODEX_BIN_PATH=/absolute/path/to/codex
|
||||
```
|
||||
2. **Config file** (`~/.archon/config.yaml`):
|
||||
```yaml
|
||||
assistants:
|
||||
codex:
|
||||
codexBinaryPath: /absolute/path/to/codex
|
||||
```
|
||||
3. **Vendor directory** (zero-config fallback): drop the native binary at `~/.archon/vendor/codex/codex` (or `codex.exe` on Windows).
|
||||
|
||||
Dev mode (`bun run`) does not require any of the above — the SDK resolves `codex` via `node_modules`.
|
||||
|
||||
### Authenticate
|
||||
|
||||
```bash
|
||||
# Install Codex CLI first: https://docs.codex.com/installation
|
||||
codex login
|
||||
|
||||
# Follow browser authentication flow
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ Set these in your shell or `.env` file:
|
|||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `CLAUDE_BIN_PATH` | Yes (binary builds) | Absolute path to the Claude Code SDK's `cli.js`. Required in compiled Archon binaries unless `assistants.claude.claudeBinaryPath` is set. Dev mode (`bun run`) auto-resolves via `node_modules`. |
|
||||
| `CLAUDE_USE_GLOBAL_AUTH` | No | Set to `true` to use credentials from `claude /login` (default when no other Claude token is set) |
|
||||
| `CLAUDE_CODE_OAUTH_TOKEN` | No | OAuth token from `claude setup-token` (alternative to global auth) |
|
||||
| `CLAUDE_API_KEY` | No | Anthropic API key for pay-per-use (alternative to global auth) |
|
||||
| `CODEX_BIN_PATH` | No | Absolute path to the Codex CLI binary. Overrides auto-detection in compiled Archon builds. |
|
||||
| `CODEX_ACCESS_TOKEN` | Yes (for Codex) | Codex access token (see [AI Assistants](/getting-started/ai-assistants/)) |
|
||||
| `DATABASE_URL` | No | PostgreSQL connection string (default: SQLite) |
|
||||
| `LOG_LEVEL` | No | `debug`, `info` (default), `warn`, `error` |
|
||||
|
|
|
|||
|
|
@ -47,6 +47,42 @@ bun install
|
|||
- [GitHub CLI](https://cli.github.com/) (`gh`)
|
||||
- [Claude Code](https://claude.ai/code) (`claude`)
|
||||
|
||||
## Claude Code is required
|
||||
|
||||
Archon orchestrates Claude Code; it does not bundle it. Install Claude Code separately:
|
||||
|
||||
```bash
|
||||
# macOS / Linux / WSL (Anthropic's recommended installer)
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://claude.ai/install.ps1 | iex
|
||||
```
|
||||
|
||||
Source installs (`bun run`) find the executable automatically via `node_modules`. Compiled binaries (quick install, Homebrew) must point at the Claude Code executable:
|
||||
|
||||
```bash
|
||||
# After the native installer:
|
||||
export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"
|
||||
|
||||
# After `npm install -g @anthropic-ai/claude-code`:
|
||||
export CLAUDE_BIN_PATH="$(npm root -g)/@anthropic-ai/claude-code/cli.js"
|
||||
```
|
||||
|
||||
Or set it durably in `~/.archon/config.yaml`:
|
||||
|
||||
```yaml
|
||||
assistants:
|
||||
claude:
|
||||
claudeBinaryPath: /absolute/path/to/claude
|
||||
```
|
||||
|
||||
Docker images (`ghcr.io/coleam00/archon`) ship with Claude Code pre-installed and
|
||||
`CLAUDE_BIN_PATH` pre-set — no configuration needed.
|
||||
|
||||
See [AI Assistants → Claude Code](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only)
|
||||
for full details and install-layout paths.
|
||||
|
||||
## Verify Installation
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Before you start, make sure you have:
|
|||
| -------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Git** | `git --version` | [git-scm.com](https://git-scm.com/) |
|
||||
| **Bun** (replaces Node.js + npm) | `bun --version` | Linux/macOS: `curl -fsSL https://bun.sh/install \| bash` — Windows: `powershell -c "irm bun.sh/install.ps1 \| iex"` |
|
||||
| **Claude Code CLI** | `claude --version` | [docs.claude.com/claude-code/installation](https://docs.claude.com/en/docs/claude-code/installation) |
|
||||
| **Claude Code CLI** | `claude --version` | [docs.claude.com/claude-code/installation](https://docs.claude.com/en/docs/claude-code/installation) — in compiled Archon binaries, also set `CLAUDE_BIN_PATH` ([details](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only)) |
|
||||
| **GitHub account** | — | [github.com](https://github.com/) |
|
||||
|
||||
> **Do not run as root.** Archon (and the Claude Code CLI it depends on) does not work when run as the `root` user. If you're on a VPS or server that only has root, create a regular user first:
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ sidebar:
|
|||
## Prerequisites
|
||||
|
||||
1. [Install Archon](/getting-started/installation/)
|
||||
2. Authenticate with Claude: run `claude /login` (uses your existing Claude Pro/Max subscription)
|
||||
3. Navigate to any git repository
|
||||
2. [Install Claude Code](/getting-started/ai-assistants/#claude-code) — Archon orchestrates it but does not bundle it
|
||||
3. Authenticate with Claude: run `claude /login` (uses your existing Claude Pro/Max subscription)
|
||||
4. In compiled Archon binaries, set `CLAUDE_BIN_PATH` (see [Binary path configuration](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only))
|
||||
5. Navigate to any git repository
|
||||
|
||||
## Run Your First Workflow
|
||||
|
||||
|
|
|
|||
|
|
@ -60,12 +60,18 @@ assistants:
|
|||
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)
|
||||
# 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)
|
||||
# or the npm-installed cli.js. Source/dev mode auto-resolves.
|
||||
# claudeBinaryPath: /absolute/path/to/claude
|
||||
codex:
|
||||
model: gpt-5.3-codex
|
||||
modelReasoningEffort: medium
|
||||
webSearchMode: disabled
|
||||
additionalDirectories:
|
||||
- /absolute/path/to/other/repo
|
||||
# codexBinaryPath: /absolute/path/to/codex # Optional: Codex CLI path
|
||||
|
||||
# Streaming preferences per platform
|
||||
streaming:
|
||||
|
|
|
|||
|
|
@ -280,6 +280,41 @@ docker compose exec app ls -la /.archon/workspaces
|
|||
docker compose exec app git clone https://github.com/user/repo /.archon/workspaces/test-repo
|
||||
```
|
||||
|
||||
## "Claude Code not found" When Running Compiled Binary
|
||||
|
||||
**Symptom:** A workflow that uses Claude fails with:
|
||||
|
||||
```
|
||||
Claude Code not found. Archon requires the Claude Code executable to be
|
||||
reachable at a configured path in compiled builds.
|
||||
```
|
||||
|
||||
**Cause:** Compiled Archon binaries (`archon` from the curl/PowerShell installer or Homebrew) do not bundle Claude Code. They need an explicit path to the Claude Code executable. Source/dev mode (`bun run`) auto-resolves via `node_modules` and is unaffected.
|
||||
|
||||
**Fix:** Install Claude Code separately and point Archon at it.
|
||||
|
||||
```bash
|
||||
# macOS / Linux / WSL — Anthropic's recommended native installer
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://claude.ai/install.ps1 | iex
|
||||
$env:CLAUDE_BIN_PATH = "$env:USERPROFILE\.local\bin\claude.exe"
|
||||
```
|
||||
|
||||
For a durable setup, set the path in `~/.archon/config.yaml` instead:
|
||||
|
||||
```yaml
|
||||
assistants:
|
||||
claude:
|
||||
claudeBinaryPath: /absolute/path/to/claude
|
||||
```
|
||||
|
||||
`archon setup` auto-detects and writes `CLAUDE_BIN_PATH` for you. Docker users do not need to do anything — the image pre-sets the variable.
|
||||
|
||||
See the [AI Assistants → Binary path configuration](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only) guide for the full install matrix.
|
||||
|
||||
## Workflows Hang Silently When Run Inside Claude Code
|
||||
|
||||
**Symptom:** Workflows started from within a Claude Code session (e.g., via the Terminal tool) produce no output, or the CLI emits a warning about `CLAUDECODE=1` before the workflow hangs.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"./types": "./src/types.ts",
|
||||
"./claude/provider": "./src/claude/provider.ts",
|
||||
"./claude/config": "./src/claude/config.ts",
|
||||
"./claude/binary-resolver": "./src/claude/binary-resolver.ts",
|
||||
"./codex/provider": "./src/codex/provider.ts",
|
||||
"./codex/config": "./src/codex/config.ts",
|
||||
"./codex/binary-resolver": "./src/codex/binary-resolver.ts",
|
||||
|
|
@ -16,7 +17,7 @@
|
|||
"./registry": "./src/registry.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test src/claude/provider.test.ts && bun test src/codex/provider.test.ts && bun test src/registry.test.ts && bun test src/codex/binary-guard.test.ts && bun test src/codex/binary-resolver.test.ts && bun test src/codex/binary-resolver-dev.test.ts",
|
||||
"test": "bun test src/claude/provider.test.ts && bun test src/codex/provider.test.ts && bun test src/registry.test.ts && bun test src/codex/binary-guard.test.ts && bun test src/codex/binary-resolver.test.ts && bun test src/codex/binary-resolver-dev.test.ts && bun test src/claude/binary-resolver.test.ts && bun test src/claude/binary-resolver-dev.test.ts",
|
||||
"type-check": "bun x tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
40
packages/providers/src/claude/binary-resolver-dev.test.ts
Normal file
40
packages/providers/src/claude/binary-resolver-dev.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Tests for the Claude binary resolver in dev mode (BUNDLED_IS_BINARY=false).
|
||||
* Separate file because binary-mode tests mock BUNDLED_IS_BINARY=true.
|
||||
*/
|
||||
import { describe, test, expect, mock } from 'bun:test';
|
||||
import { createMockLogger } from '../test/mocks/logger';
|
||||
|
||||
mock.module('@archon/paths', () => ({
|
||||
createLogger: mock(() => createMockLogger()),
|
||||
BUNDLED_IS_BINARY: false,
|
||||
}));
|
||||
|
||||
import { resolveClaudeBinaryPath } from './binary-resolver';
|
||||
|
||||
describe('resolveClaudeBinaryPath (dev mode)', () => {
|
||||
test('returns undefined when BUNDLED_IS_BINARY is false', async () => {
|
||||
const result = await resolveClaudeBinaryPath();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns undefined even with config path set', async () => {
|
||||
const result = await resolveClaudeBinaryPath('/some/custom/path');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns undefined even with env var set', async () => {
|
||||
const original = process.env.CLAUDE_BIN_PATH;
|
||||
process.env.CLAUDE_BIN_PATH = '/some/env/path';
|
||||
try {
|
||||
const result = await resolveClaudeBinaryPath();
|
||||
expect(result).toBeUndefined();
|
||||
} finally {
|
||||
if (original !== undefined) {
|
||||
process.env.CLAUDE_BIN_PATH = original;
|
||||
} else {
|
||||
delete process.env.CLAUDE_BIN_PATH;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
91
packages/providers/src/claude/binary-resolver.test.ts
Normal file
91
packages/providers/src/claude/binary-resolver.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Tests for the Claude binary resolver in binary mode.
|
||||
*
|
||||
* Must run in its own bun test invocation because it mocks @archon/paths
|
||||
* with BUNDLED_IS_BINARY=true, which conflicts with other test files.
|
||||
*/
|
||||
import { describe, test, expect, mock, beforeEach, afterAll, spyOn } from 'bun:test';
|
||||
import { createMockLogger } from '../test/mocks/logger';
|
||||
|
||||
const mockLogger = createMockLogger();
|
||||
|
||||
// Mock @archon/paths with BUNDLED_IS_BINARY = true (binary mode)
|
||||
mock.module('@archon/paths', () => ({
|
||||
createLogger: mock(() => mockLogger),
|
||||
BUNDLED_IS_BINARY: true,
|
||||
}));
|
||||
|
||||
import * as resolver from './binary-resolver';
|
||||
|
||||
describe('resolveClaudeBinaryPath (binary mode)', () => {
|
||||
const originalEnv = process.env.CLAUDE_BIN_PATH;
|
||||
let fileExistsSpy: ReturnType<typeof spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.CLAUDE_BIN_PATH;
|
||||
fileExistsSpy?.mockRestore();
|
||||
mockLogger.info.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.CLAUDE_BIN_PATH = originalEnv;
|
||||
} else {
|
||||
delete process.env.CLAUDE_BIN_PATH;
|
||||
}
|
||||
fileExistsSpy?.mockRestore();
|
||||
});
|
||||
|
||||
test('uses CLAUDE_BIN_PATH env var when set and file exists', async () => {
|
||||
process.env.CLAUDE_BIN_PATH = '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js';
|
||||
fileExistsSpy = spyOn(resolver, 'fileExists').mockReturnValue(true);
|
||||
|
||||
const result = await resolver.resolveClaudeBinaryPath();
|
||||
expect(result).toBe('/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js');
|
||||
});
|
||||
|
||||
test('throws when CLAUDE_BIN_PATH is set but file does not exist', async () => {
|
||||
process.env.CLAUDE_BIN_PATH = '/nonexistent/cli.js';
|
||||
fileExistsSpy = spyOn(resolver, 'fileExists').mockReturnValue(false);
|
||||
|
||||
await expect(resolver.resolveClaudeBinaryPath()).rejects.toThrow(
|
||||
'CLAUDE_BIN_PATH is set to "/nonexistent/cli.js" but the file does not exist'
|
||||
);
|
||||
});
|
||||
|
||||
test('uses config claudeBinaryPath when file exists', async () => {
|
||||
fileExistsSpy = spyOn(resolver, 'fileExists').mockReturnValue(true);
|
||||
|
||||
const result = await resolver.resolveClaudeBinaryPath('/custom/claude/cli.js');
|
||||
expect(result).toBe('/custom/claude/cli.js');
|
||||
});
|
||||
|
||||
test('throws when config claudeBinaryPath file does not exist', async () => {
|
||||
fileExistsSpy = spyOn(resolver, 'fileExists').mockReturnValue(false);
|
||||
|
||||
await expect(resolver.resolveClaudeBinaryPath('/nonexistent/cli.js')).rejects.toThrow(
|
||||
'assistants.claude.claudeBinaryPath is set to "/nonexistent/cli.js" but the file does not exist'
|
||||
);
|
||||
});
|
||||
|
||||
test('env var takes precedence over config path', async () => {
|
||||
process.env.CLAUDE_BIN_PATH = '/env/cli.js';
|
||||
fileExistsSpy = spyOn(resolver, 'fileExists').mockReturnValue(true);
|
||||
|
||||
const result = await resolver.resolveClaudeBinaryPath('/config/cli.js');
|
||||
expect(result).toBe('/env/cli.js');
|
||||
});
|
||||
|
||||
test('throws with install instructions when nothing configured', async () => {
|
||||
fileExistsSpy = spyOn(resolver, 'fileExists').mockReturnValue(false);
|
||||
|
||||
const promise = resolver.resolveClaudeBinaryPath();
|
||||
await expect(promise).rejects.toThrow('Claude Code not found');
|
||||
await expect(promise).rejects.toThrow('CLAUDE_BIN_PATH');
|
||||
// Native curl installer is Anthropic's primary recommendation.
|
||||
await expect(promise).rejects.toThrow('https://claude.ai/install.sh');
|
||||
// npm path is still documented as an alternative.
|
||||
await expect(promise).rejects.toThrow('npm install -g @anthropic-ai/claude-code');
|
||||
await expect(promise).rejects.toThrow('claudeBinaryPath');
|
||||
});
|
||||
});
|
||||
94
packages/providers/src/claude/binary-resolver.ts
Normal file
94
packages/providers/src/claude/binary-resolver.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Claude Code CLI resolver for compiled (bun --compile) archon binaries.
|
||||
*
|
||||
* The @anthropic-ai/claude-agent-sdk spawns a subprocess using
|
||||
* `pathToClaudeCodeExecutable`. In dev mode the SDK resolves this from its
|
||||
* own node_modules location; in compiled binaries that path is frozen to
|
||||
* the build host's filesystem and does not exist on end-user machines.
|
||||
*
|
||||
* Resolution order (binary mode only):
|
||||
* 1. `CLAUDE_BIN_PATH` environment variable
|
||||
* 2. `assistants.claude.claudeBinaryPath` in config
|
||||
* 3. Throw with install instructions
|
||||
*
|
||||
* In dev mode (BUNDLED_IS_BINARY=false), returns undefined so the caller
|
||||
* omits `pathToClaudeCodeExecutable` entirely and the SDK resolves via its
|
||||
* normal node_modules lookup.
|
||||
*/
|
||||
import { existsSync as _existsSync } from 'node:fs';
|
||||
import { BUNDLED_IS_BINARY, createLogger } from '@archon/paths';
|
||||
|
||||
/** Wrapper for existsSync — enables spyOn in tests (direct imports can't be spied on). */
|
||||
export function fileExists(path: string): boolean {
|
||||
return _existsSync(path);
|
||||
}
|
||||
|
||||
/** Lazy-initialized logger */
|
||||
let cachedLog: ReturnType<typeof createLogger> | undefined;
|
||||
function getLog(): ReturnType<typeof createLogger> {
|
||||
if (!cachedLog) cachedLog = createLogger('claude-binary');
|
||||
return cachedLog;
|
||||
}
|
||||
|
||||
const INSTALL_INSTRUCTIONS =
|
||||
'Claude Code not found. Archon requires the Claude Code executable to be\n' +
|
||||
'reachable at a configured path in compiled builds.\n\n' +
|
||||
'To fix, install Claude Code and point Archon at it:\n\n' +
|
||||
' macOS / Linux (recommended — native installer):\n' +
|
||||
' curl -fsSL https://claude.ai/install.sh | bash\n' +
|
||||
' export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"\n\n' +
|
||||
' Windows (PowerShell):\n' +
|
||||
' irm https://claude.ai/install.ps1 | iex\n' +
|
||||
' $env:CLAUDE_BIN_PATH = "$env:USERPROFILE\\.local\\bin\\claude.exe"\n\n' +
|
||||
' Or via npm (alternative):\n' +
|
||||
' npm install -g @anthropic-ai/claude-code\n' +
|
||||
' export CLAUDE_BIN_PATH="$(npm root -g)/@anthropic-ai/claude-code/cli.js"\n\n' +
|
||||
'Persist the path in ~/.archon/config.yaml instead of the env var:\n' +
|
||||
' assistants:\n' +
|
||||
' claude:\n' +
|
||||
' claudeBinaryPath: /absolute/path/to/claude\n\n' +
|
||||
'See: https://archon.diy/docs/reference/configuration#claude';
|
||||
|
||||
/**
|
||||
* Resolve the path to the Claude Code SDK's cli.js.
|
||||
*
|
||||
* In dev mode: returns undefined (let SDK resolve via node_modules).
|
||||
* In binary mode: resolves from env/config, or throws with install instructions.
|
||||
*/
|
||||
export async function resolveClaudeBinaryPath(
|
||||
configClaudeBinaryPath?: string
|
||||
): Promise<string | undefined> {
|
||||
if (!BUNDLED_IS_BINARY) return undefined;
|
||||
|
||||
// 1. Environment variable override
|
||||
const envPath = process.env.CLAUDE_BIN_PATH;
|
||||
if (envPath) {
|
||||
if (!fileExists(envPath)) {
|
||||
throw new Error(
|
||||
`CLAUDE_BIN_PATH is set to "${envPath}" but the file does not exist.\n` +
|
||||
'Please verify the path points to the Claude Code executable (native binary\n' +
|
||||
'from the curl/PowerShell installer, or cli.js from an npm global install).'
|
||||
);
|
||||
}
|
||||
getLog().info({ binaryPath: envPath, source: 'env' }, 'claude.binary_resolved');
|
||||
return envPath;
|
||||
}
|
||||
|
||||
// 2. Config file override
|
||||
if (configClaudeBinaryPath) {
|
||||
if (!fileExists(configClaudeBinaryPath)) {
|
||||
throw new Error(
|
||||
`assistants.claude.claudeBinaryPath is set to "${configClaudeBinaryPath}" but the file does not exist.\n` +
|
||||
'Please verify the path in .archon/config.yaml points to the Claude Code executable.'
|
||||
);
|
||||
}
|
||||
getLog().info(
|
||||
{ binaryPath: configClaudeBinaryPath, source: 'config' },
|
||||
'claude.binary_resolved'
|
||||
);
|
||||
return configClaudeBinaryPath;
|
||||
}
|
||||
|
||||
// 3. Not found — throw with install instructions
|
||||
throw new Error(INSTALL_INSTRUCTIONS);
|
||||
}
|
||||
|
|
@ -27,5 +27,9 @@ export function parseClaudeConfig(raw: Record<string, unknown>): ClaudeProviderD
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof raw.claudeBinaryPath === 'string') {
|
||||
result.claudeBinaryPath = raw.claudeBinaryPath;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,39 @@ mock.module('@anthropic-ai/claude-agent-sdk', () => ({
|
|||
query: mockQuery,
|
||||
}));
|
||||
|
||||
import { ClaudeProvider } from './provider';
|
||||
import { ClaudeProvider, shouldPassNoEnvFile } from './provider';
|
||||
import * as claudeModule from './provider';
|
||||
|
||||
describe('shouldPassNoEnvFile', () => {
|
||||
test('returns true when cliPath is undefined (dev mode — SDK spawns cli.js via Bun)', () => {
|
||||
expect(shouldPassNoEnvFile(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true for an explicit cli.js path (npm-installed, SDK spawns via Bun/Node)', () => {
|
||||
expect(
|
||||
shouldPassNoEnvFile('/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for a native binary path (curl installer, SDK execs directly)', () => {
|
||||
expect(shouldPassNoEnvFile('/Users/test/.local/bin/claude')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for a Windows native binary path', () => {
|
||||
expect(shouldPassNoEnvFile('C:\\Users\\test\\.local\\bin\\claude.exe')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for a Homebrew symlink path', () => {
|
||||
expect(shouldPassNoEnvFile('/opt/homebrew/bin/claude')).toBe(false);
|
||||
});
|
||||
|
||||
test('extension match is suffix-only (paths ending in cli.js but not literally `.js` extension are still rejected)', () => {
|
||||
// Defensive: only string-suffix matches `.js` count as JS executables.
|
||||
expect(shouldPassNoEnvFile('/path/to/cli.json')).toBe(false);
|
||||
expect(shouldPassNoEnvFile('/path/to/cli.js.bak')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClaudeProvider', () => {
|
||||
let client: ClaudeProvider;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@
|
|||
* - CLAUDE_USE_GLOBAL_AUTH=true: Use global auth from `claude /login`, filter env tokens
|
||||
* - CLAUDE_USE_GLOBAL_AUTH=false: Use explicit tokens from env vars
|
||||
* - Not set: Auto-detect - use tokens if present in env, otherwise global auth
|
||||
*
|
||||
* Binary resolution:
|
||||
* - In compiled binaries, `pathToClaudeCodeExecutable` is resolved from
|
||||
* `CLAUDE_BIN_PATH` env or `assistants.claude.claudeBinaryPath` config;
|
||||
* see ./binary-resolver.ts. In dev mode the SDK resolves cli.js itself
|
||||
* from node_modules.
|
||||
*/
|
||||
import {
|
||||
query,
|
||||
|
|
@ -18,7 +24,6 @@ import {
|
|||
type HookCallback,
|
||||
type HookCallbackMatcher,
|
||||
} from '@anthropic-ai/claude-agent-sdk';
|
||||
import cliPath from '@anthropic-ai/claude-agent-sdk/embed';
|
||||
import type {
|
||||
IAgentProvider,
|
||||
SendQueryOptions,
|
||||
|
|
@ -29,6 +34,7 @@ import type {
|
|||
} from '../types';
|
||||
import { parseClaudeConfig } from './config';
|
||||
import { CLAUDE_CAPABILITIES } from './capabilities';
|
||||
import { resolveClaudeBinaryPath } from './binary-resolver';
|
||||
import { createLogger } from '@archon/paths';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { resolve, isAbsolute } from 'path';
|
||||
|
|
@ -499,6 +505,33 @@ interface ToolResultEntry {
|
|||
toolCallId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the Claude subprocess should be spawned with `--no-env-file`.
|
||||
*
|
||||
* `--no-env-file` is a Bun flag that prevents auto-loading `.env` from the
|
||||
* target repo cwd into the spawned process. It only applies when the SDK
|
||||
* spawns the executable via Bun/Node — i.e. when the executable is a `.js`
|
||||
* file (dev mode resolves cli.js, npm-installed resolves cli.js). For a
|
||||
* native Claude Code binary (curl/PowerShell installer at
|
||||
* `~/.local/bin/claude`), the SDK execs the binary directly and the flag
|
||||
* gets passed to the native binary, which rejects unknown options and
|
||||
* exits code 1.
|
||||
*
|
||||
* Returning `false` for native binaries is verified safe — the native
|
||||
* binary does not auto-load `.env` from CWD (probed end-to-end with
|
||||
* sentinel `.env` and `.env.local` in the workflow CWD; both arrived
|
||||
* UNSET in the spawned bash tool). The first-layer protection —
|
||||
* `stripCwdEnv()` in `@archon/paths` (#1067) — removes CWD env keys from
|
||||
* the parent process before spawn, so the subprocess inherits a clean
|
||||
* env regardless of executable type.
|
||||
*
|
||||
* Exported so the decision can be unit-tested without needing to mock
|
||||
* `BUNDLED_IS_BINARY` or run the full provider sendQuery pathway.
|
||||
*/
|
||||
export function shouldPassNoEnvFile(cliPath: string | undefined): boolean {
|
||||
return cliPath === undefined || cliPath.endsWith('.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build base Claude SDK options from cwd, request options, and assistant defaults.
|
||||
* Does not include nodeConfig translation — that is handled by applyNodeConfig.
|
||||
|
|
@ -510,14 +543,21 @@ function buildBaseClaudeOptions(
|
|||
controller: AbortController,
|
||||
stderrLines: string[],
|
||||
toolResultQueue: ToolResultEntry[],
|
||||
env: NodeJS.ProcessEnv
|
||||
env: NodeJS.ProcessEnv,
|
||||
cliPath: string | undefined
|
||||
): Options {
|
||||
const isJsExecutable = shouldPassNoEnvFile(cliPath);
|
||||
getLog().debug(
|
||||
{ cliPath: cliPath ?? null, isJsExecutable, passesNoEnvFile: isJsExecutable },
|
||||
'claude.subprocess_env_file_flag'
|
||||
);
|
||||
|
||||
return {
|
||||
cwd,
|
||||
pathToClaudeCodeExecutable: cliPath,
|
||||
// Prevent Bun from auto-loading .env from the target repo cwd.
|
||||
// Without this, the Claude Code subprocess inherits repo secrets.
|
||||
executableArgs: ['--no-env-file'],
|
||||
// In compiled binaries, the resolver supplies an absolute executable path;
|
||||
// in dev mode it returns undefined and the SDK resolves from node_modules.
|
||||
...(cliPath !== undefined ? { pathToClaudeCodeExecutable: cliPath } : {}),
|
||||
...(isJsExecutable ? { executableArgs: ['--no-env-file'] } : {}),
|
||||
env,
|
||||
model: requestOptions?.model ?? assistantDefaults.model,
|
||||
abortController: controller,
|
||||
|
|
@ -840,6 +880,11 @@ export class ClaudeProvider implements IAgentProvider {
|
|||
let lastError: Error | undefined;
|
||||
const assistantDefaults = parseClaudeConfig(requestOptions?.assistantConfig ?? {});
|
||||
|
||||
// Resolve Claude CLI path once before the retry loop. In binary mode this
|
||||
// throws immediately if neither env nor config supplies a valid path, so
|
||||
// the user gets a clean error rather than N retries of "Module not found".
|
||||
const resolvedCliPath = await resolveClaudeBinaryPath(assistantDefaults.claudeBinaryPath);
|
||||
|
||||
// Build subprocess env once (avoids re-logging auth mode per retry)
|
||||
const subprocessEnv = buildSubprocessEnv();
|
||||
const env = requestOptions?.env ? { ...subprocessEnv, ...requestOptions.env } : subprocessEnv;
|
||||
|
|
@ -879,7 +924,7 @@ export class ClaudeProvider implements IAgentProvider {
|
|||
const controller = new AbortController();
|
||||
currentController = controller;
|
||||
|
||||
// 1. Build SDK options (env pre-computed above)
|
||||
// 1. Build SDK options (env and cliPath pre-computed above)
|
||||
const options = buildBaseClaudeOptions(
|
||||
cwd,
|
||||
requestOptions,
|
||||
|
|
@ -887,7 +932,8 @@ export class ClaudeProvider implements IAgentProvider {
|
|||
controller,
|
||||
stderrLines,
|
||||
toolResultQueue,
|
||||
env
|
||||
env,
|
||||
resolvedCliPath
|
||||
);
|
||||
|
||||
// 2. Apply nodeConfig translation (re-applied per attempt since options are fresh)
|
||||
|
|
|
|||
|
|
@ -42,4 +42,5 @@ export { parseCodexConfig, type CodexProviderDefaults } from './codex/config';
|
|||
|
||||
// Utilities (needed by consumers)
|
||||
export { resetCodexSingleton } from './codex/provider';
|
||||
export { resolveCodexBinaryPath, fileExists } from './codex/binary-resolver';
|
||||
export { resolveCodexBinaryPath, fileExists as codexFileExists } from './codex/binary-resolver';
|
||||
export { resolveClaudeBinaryPath, fileExists as claudeFileExists } from './claude/binary-resolver';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export interface ClaudeProviderDefaults {
|
|||
* @default ['project']
|
||||
*/
|
||||
settingSources?: ('project' | 'user')[];
|
||||
/** Absolute path to the Claude Code SDK's `cli.js`. Required in compiled
|
||||
* Archon builds when `CLAUDE_BIN_PATH` is not set; optional in dev mode
|
||||
* (SDK resolves from node_modules). */
|
||||
claudeBinaryPath?: string;
|
||||
}
|
||||
|
||||
export interface CodexProviderDefaults {
|
||||
|
|
|
|||
Loading…
Reference in a new issue