Adjutant supports two LLM backends. You choose one in adjutant.yaml and all AI interactions route through it.
| Backend | Binary | Auth | Billing |
|---|---|---|---|
OpenCode (opencode) |
opencode CLI |
Anthropic API key | Pay-per-token |
Claude Code CLI (claude-cli) |
claude CLI |
Claude subscription (Pro/Team/Enterprise) | Included in subscription |
Set llm.backend in adjutant.yaml:
llm:
backend: "opencode" # or "claude-cli"Or use the setup wizard, which will ask during initial configuration:
adjutant setup- You have an Anthropic API key
- You want streaming output
- You want process reaping for orphan cleanup
- You have a Claude Pro, Team, or Enterprise subscription
- You prefer subscription-based billing over pay-per-token
- You want per-request cost tracking (Claude CLI reports
cost_usdin JSON output)
- Edit
adjutant.yaml:llm: backend: "claude-cli" # was "opencode"
- Restart Adjutant:
adjutant restart
On restart, Adjutant detects the backend change and automatically:
- Translates the active model ID to the new backend's format
- Clears the Telegram session (new backend, new conversation)
- Records the switch in
state/backend.txt
No data is lost. You can switch back at any time.
| Capability | OpenCode | Claude CLI |
|---|---|---|
| Vision (image analysis) | Yes (native) | Yes (via Read tool image injection) |
| Dynamic model listing | Yes | Yes |
| Process reaping | Yes | No (not needed) |
| Backend-native web server | No | No |
| Streaming output | Yes | No (single-shot JSON) |
| Cost tracking per request | No | Yes |
| Session resume | Yes (--session) |
Yes (--resume) |
If you use a feature that the current backend doesn't support, Adjutant tells you clearly rather than failing silently.
npm install -g @anthropic-ai/claude-codeclaude login# adjutant.yaml
llm:
backend: "claude-cli"adjutant doctorThe doctor command checks that the claude binary is on PATH, hooks are executable, and the backend is healthy.
Use adjutant web to start Adjutant's own web dashboard from the monorepo checkout. Backend-native web servers are retired on both backends.
Claude Code requires explicit permission handling for non-interactive (subprocess) use. Adjutant supports two modes:
llm:
backend: "claude-cli"
permission_mode: "skip"Uses --dangerously-skip-permissions. This bypasses Claude Code's deny rules in .claude/settings.json, but hooks still fire. The hooks in .claude/hooks/ are the primary technical defense for .env protection.
llm:
backend: "claude-cli"
permission_mode: "allowlist"
allowed_tools: "Read,Glob,Grep,Edit,Write,Bash(*)"Uses --allowedTools with an explicit whitelist. More restrictive but requires listing every tool the agent needs.
Both backends protect .env files through layered defenses:
| Layer | OpenCode | Claude CLI |
|---|---|---|
| Workspace permissions | opencode.json deny rules |
.claude/settings.json (bypassed by --dangerously-skip-permissions) |
| Hooks | N/A | .claude/hooks/block-env-access.sh + block-env-read.sh (always active) |
| Agent instructions | "Never read .env" in agent prompt | Same |
On Claude CLI, the hooks are the primary defense. They fire even when --dangerously-skip-permissions is active, blocking:
cat .env,head .env,tail .env, and similar read commandssource .envand. .envprintenv,env,export -pgrep,awk,sedon.envfiles- Read tool calls targeting
.envpaths
Switching backends is transparent for all KB interactions:
- Adjutant querying KBs (
kb query,kb write): Handled automatically by the backend abstraction inquery.py. - KB-internal LLM calls: KBs with their own LLM pipelines (e.g. portfolio-kb's analyze pipeline) read the active backend from
adjutant.yamlvia theADJ_DIRenv var and dispatch to the appropriate binary. Switchingllm.backendswitches both Adjutant and KB-internal calls. - Workspace configs: Both
opencode.jsonand.claude/settings.jsonare scaffolded during KB creation. When switching backends,_handle_backend_switch()auto-generates any missing scaffold files.
See docs/development/backend-guide.md for the implementation pattern for KB-internal LLM calls.
- Response style: Claude CLI responses may feel different from OpenCode responses because OpenCode uses streaming output while Claude CLI uses single-shot JSON mode. This affects Telegram delivery — OpenCode responses can arrive incrementally, Claude CLI delivers the full response at once.
- Vision: OpenCode passes images natively. Claude CLI uses Read tool image injection (the image is read into the prompt context). Both work, but the mechanisms differ.
- Model names: OpenCode uses full IDs (
anthropic/claude-sonnet-4-6), Claude CLI uses short names (sonnet). Adjutant translates automatically. - Cost tracking: Only Claude CLI reports
cost_usd. OpenCode responses have no cost field. - Process cleanup: OpenCode spawns
bash-language-serverchild processes that need reaping. Claude CLI does not have this issue.
Install Claude Code CLI: npm install -g @anthropic-ai/claude-code
Or set the CLAUDE_CODE_BIN environment variable to the full path:
export CLAUDE_CODE_BIN=/usr/local/bin/claudeRun claude login and follow the prompts.
Check that hook scripts are executable:
chmod +x .claude/hooks/block-env-access.sh
chmod +x .claude/hooks/block-env-read.shadjutant doctor flags non-executable hooks as errors.
Restart Adjutant after changing llm.backend:
adjutant restartCheck state/backend.txt to confirm the active backend.