When to use claudecli
claudecli shells out to the claude CLI (Claude Code) as a subprocess. It is the default provider and the right choice when:
- You already have Claude Code installed and authenticated locally.
- You want to reuse a subscription-tier Claude Code account rather than plumb API keys.
- You want the model to run inside
claude's own tool-use loop (its file-editing, thinking, and plan mode features are intact). - You want zero secret material in rousseau's config file.
The trade-off: rousseau's tool Registry is not invoked for this provider — claude runs its own tools inside the subprocess. Response objects come back as a single end-of-turn text message. If you need rousseau to gate bash/edit/write through the approval policy, use anthropic, bedrock, vertex, or an OpenAI-compatible provider instead.
Auth inheritance
The claude CLI holds authentication in three places:
| Location | Contents |
|---|---|
~/.claude/ |
OAuth tokens (subscription), API-key helper output, workspace config. |
| System keychain | On macOS, claude may cache refresh tokens in the login keychain. |
ANTHROPIC_API_KEY env |
If set, claude uses it for API-key mode instead of OAuth. |
claudecli never reads these directly. Every invocation is exec.CommandContext(binary, args...) — the subprocess inherits the parent's environment and home directory, and looks up its own credentials. That is what makes it "zero-config" for individual operators.
Volume=%h/.claude:/home/rousseau/.claude:rw,Z
The Z label is critical on SELinux hosts; see Deployment for the full Quadlet unit.
Configuration
provider: claudecli
claudecli:
binary: claude
model: sonnet
permission_mode: bypassPermissions
extra_args:
- --add-dir
- /workspace
| Field | Default | Effect |
|---|---|---|
binary |
claude |
Executable resolved on $PATH. Point at an absolute path if you have multiple claude versions. |
model |
empty | Passed as --model <value>. Empty uses claude's default. |
permission_mode |
empty | Passed as --permission-mode <value>. See table below. |
extra_args |
[] |
Prepended before -p <prompt> on every invocation. |
Every field maps to ClaudeCLIConfig in internal/config/config.go. The subprocess command line assembled at each turn is:
claude --print --output-format json \
--session-id <sessionID> \
--system-prompt <systemPrompt> \
--model <model> \
--permission-mode <permissionMode> \
<extra_args...> \
<prompt>
PermissionMode matrix
The PermissionMode flag mirrors claude's own --permission-mode. The subprocess enforces the value; rousseau does not double-check.
Interactive TUI sessions where a human sits at the terminal and can approve tool calls.
| Mode | Behaviour |
|---|---|
default |
Claude Code prompts interactively for every tool call. Best for exploratory sessions. |
acceptEdits |
File edits proceed without prompting; other tools still prompt. Good when you trust the edit surface. |
auto |
Automatic based on the tool. Use when you want claude's built-in heuristic to decide. |
claudecli:
permission_mode: acceptEdits
Chat transports (WhatsApp, Slack, Discord, Signal, …) have no human at the terminal to answer prompts.
| Mode | Behaviour |
|---|---|
bypassPermissions |
Every tool call runs without prompting. Accepts full blast radius. |
dontAsk |
Alias treated similarly to bypass. |
claudecli:
permission_mode: bypassPermissions
The CLI sets bypassPermissions automatically for unattended daemons if the operator did not specify one — see setUnattendedPermissionDefault in internal/cli.
Exploration mode for large refactors or code reviews where you do not want any writes.
| Mode | Behaviour |
|---|---|
plan |
Planner mode. Reads and grep are allowed; writes are inhibited. |
claudecli:
permission_mode: plan
Pair with rousseau's own read-only mode (see Guides: Read-only mode) for belt-and-braces enforcement.
Session correlation
claudecli maintains conversation state inside the subprocess. Rousseau correlates its own session IDs with claude's via two flags:
claude -p --session-id <uuid>creates a new session. If the UUID already exists,claudeerrors withalready in use.claude -p --resume <uuid>resumes an existing session. If unknown,claudeerrors.
Rousseau picks the flag using an in-memory SessionCache (InMemorySessionCache by default). On a cold-start cache miss where claude already has state from a previous rousseau run, the provider optimistically tries --session-id, catches the already in use error, and retries with --resume. See the comment on (*Provider).Complete in internal/llm/claudecli/client.go.
Callers embedding the provider can swap in a persistent cache via provider.WithCache(store) — the state.sqlite store implements the same interface and survives daemon restarts, avoiding the cold-start roundtrip on the first turn after a reboot.
Model aliases
claude's model aliases are honoured by the subprocess unchanged:
| Alias | Points at |
|---|---|
sonnet |
The current default Sonnet-tier model. |
opus |
The current default Opus-tier model. |
haiku |
The current default Haiku-tier model. |
For reproducibility across daemon restarts (skill benchmarks, cron jobs, batch runs), pin an exact model ID:
claudecli:
model: claude-sonnet-4-6
Combining with skills
claudecli sends the system prompt via --system-prompt on session creation. claude honours it verbatim and ignores subsequent --system-prompt values on --resume — which matches how rousseau uses it. The SkillsProvider output is spliced in before the invocation:
<agent.SystemPrompt>
<skill 1 markdown>
<skill 2 markdown>
<RecallProvider appendix>
See internal/agent/agent.go systemPrompt(). Skills work identically across every provider; the mechanics of the composition happen in agent.Agent, not the provider.
Gotchas
- No cross-provider portability. A session created against
claudecliis not portable toanthropic— model-side state lives insideclaude. Switching providers midway forces a new session. - Tool registry is not invoked.
bash,edit,write,grep,readare executed byclaude, not byrousseau. Rousseau'sagent.Approvercannot gate those calls. Use a non-claudecliprovider if you need rousseau-side approval enforcement. --add-dirscoping. By defaultclauderefuses to read outside its own workspace. Pass--add-dir /workspace(or wherever your source lives) viaextra_argsto widen it. Combine with rousseau's approval policy at the transport level if you want to compensate for the loss of control.- Streaming.
claudecliusesclaude -p --output-format json(non-streaming). The streaming path ininternal/llm/claudecli/stream.goreads--output-format stream-json; opt in by usingStreamingProviderfrom an embedding integration. - Environment leakage. The subprocess inherits every environment variable of the parent. If
ANTHROPIC_API_KEYis set in rousseau's environment,claudewill prefer it over cached OAuth. That is usually fine, but it changes billing.
Troubleshooting
claudecli: run: exec: "claude": executable file not found in $PATH
claude is not on PATH (or the container image does not ship it). Two fixes:
- Set
claudecli.binaryto an absolute path. - Add Claude Code to the container's runtime layer — the reference
docker/Dockerfileusesnode:22-alpinefor this reason.
claudecli: model error: session id already in use
You are running two rousseau processes against the same session ID against the same claude install, or the in-memory cache dropped a session that claude still remembers. The optimistic retry described above handles the second case; the first means you have concurrent daemons stepping on each other.
claudecli: no JSON in output
claude printed non-JSON to stdout, or exited before emitting the envelope. Common causes: an invalid API key on the Claude Code side, a claude version that predates --output-format json, or a shell wrapper writing progress markers. Run claude -p --output-format json 'hello' directly to isolate.
The reply cuts off mid-sentence
claude's output is capped by --max-turns and its own internal token budget. Rousseau does not set --max-turns; if you set it via extra_args, raise it. For long generations, consider a direct API provider where you control MaxTokens from internal/llm/anthropic/client.go.
Subscription plan is rate-limited but the API is fine
The claude CLI on a subscription plan has hidden per-conversation and per-window limits. If you hit them, switch to provider: anthropic with an API key — the direct API has explicit, published limits (see Guides: Rate limits).
Related pages
- Providers: Anthropic — direct API with prompt caching and streaming.
- Providers: Bedrock — AWS-managed Claude.
- User Guide: Approval Policies — how to gate tool calls at the rousseau layer.
- Skills — how the system-prompt appendix is composed.
- Configuration — the
claudeclistanza in context.
Further reading
internal/llm/claudecli/client.go— subprocess invocation, session correlation, JSON parsing.internal/llm/claudecli/stream.go— streaming variant using--output-format stream-json.internal/config/config.go—ClaudeCLIConfigstruct.internal/cli/root.go— howsetUnattendedPermissionDefaultpicksbypassPermissionsfor chat transports.