11 KiB
Claude Code Research
Research notes on Claude Code's configuration, credential discovery, and runtime behavior based on agent-jj implementation.
Overview
- Provider: Anthropic
- Execution Method: CLI subprocess (
claudecommand) - Session Persistence: Session ID (string)
- SDK: None (spawns CLI directly)
ACP Terminology (Sandbox Agent v1)
Use these terms consistently when discussing Claude's ACP path:
| Term | Meaning |
|---|---|
| ACP agent process launcher | The command used to start the ACP agent process, commonly an npx launcher script that executes claude-code-acp. |
| ACP agent process | The running ACP agent process spawned by Sandbox Agent from the launcher command. |
| ACP client | The client-visible transport handle identified by X-ACP-Connection-Id; requests and SSE are scoped to this ACP client. |
Related IDs:
- ACP client ID: value of
X-ACP-Connection-Id(transport identity). - ACP session ID:
sessionIdreturned bysession/new(conversation/session identity within ACP).
Credential Discovery
Priority Order
- User-configured credentials (passed as
ANTHROPIC_API_KEYenv var) - Environment variables:
ANTHROPIC_API_KEYorCLAUDE_API_KEY - Bootstrap extraction from config files
- OAuth fallback (Claude CLI handles internally)
Config File Locations
| Path | Description |
|---|---|
~/.claude.json.api |
API key config (highest priority) |
~/.claude.json |
General config |
~/.claude.json.nathan |
User-specific backup (custom) |
~/.claude/.credentials.json |
OAuth credentials |
~/.claude-oauth-credentials.json |
Docker mount alternative for OAuth |
API Key Field Names (checked in order)
{
"primaryApiKey": "sk-ant-...",
"apiKey": "sk-ant-...",
"anthropicApiKey": "sk-ant-...",
"customApiKey": "sk-ant-..."
}
Keys must start with sk-ant- prefix to be valid.
OAuth Structure
// ~/.claude/.credentials.json
{
"claudeAiOauth": {
"accessToken": "...",
"expiresAt": "2024-01-01T00:00:00Z"
}
}
OAuth tokens are validated for expiry before use.
CLI Invocation
Command Structure
claude \
--print \
--output-format stream-json \
--verbose \
--dangerously-skip-permissions \
[--resume SESSION_ID] \
[--model MODEL_ID] \
[--permission-mode plan] \
"PROMPT"
Arguments
| Flag | Description |
|---|---|
--print |
Output mode |
--output-format stream-json |
Newline-delimited JSON streaming |
--verbose |
Verbose output |
--dangerously-skip-permissions |
Skip permission prompts |
--resume SESSION_ID |
Resume existing session |
--model MODEL_ID |
Specify model (e.g., claude-sonnet-4-20250514) |
--permission-mode plan |
Plan mode (read-only exploration) |
Environment Variables
Only ANTHROPIC_API_KEY is passed if an API key is found. If no key is found, Claude CLI uses its built-in OAuth flow from ~/.claude/.credentials.json.
Streaming Response Format
Claude CLI outputs newline-delimited JSON events:
{"type": "assistant", "message": {"content": [{"type": "text", "text": "..."}]}}
{"type": "tool_use", "tool_use": {"name": "Read", "input": {...}}}
{"type": "result", "result": "Final response text", "session_id": "abc123"}
Event Types
| Type | Description |
|---|---|
assistant |
Assistant message with content blocks |
tool_use |
Tool invocation |
tool_result |
Tool result (may include is_error) |
result |
Final result with session ID |
Content Block Types
{
type: "text" | "tool_use";
text?: string;
name?: string; // tool name
input?: object; // tool input
}
Limitations (Headless CLI)
- The headless CLI tool list does not include the
AskUserQuestiontool, even though the Agent SDK documents it. - As a result, prompting the CLI to "call AskUserQuestion" does not emit question events; it proceeds with normal tool/message flow instead.
- If we need structured question events, we can implement a wrapper around the Claude Agent SDK (instead of the CLI) and surface question events in our own transport.
- The current Python SDK repo does not expose
AskUserQuestiontypes; it only supports permission prompts via the control protocol.
Response Schema
// ClaudeCliResponseSchema
{
result?: string; // Final response text
session_id?: string; // Session ID for resumption
structured_output?: unknown; // Optional structured output
error?: unknown; // Error information
}
Session Management
- Session ID is captured from streaming events (
event.session_id) - Use
--resume SESSION_IDto continue a session - Sessions are stored internally by Claude CLI
Timeout
- Default timeout: 5 minutes (300,000 ms)
- Process is killed with
SIGTERMon timeout
Agent Modes vs Permission Modes
Claude conflates agent mode and permission mode - plan is a permission restriction that forces planning behavior.
Permission Modes
| Mode | CLI Flag | Behavior |
|---|---|---|
default |
(none) | Normal permission prompts |
acceptEdits |
--permission-mode acceptEdits |
Auto-accept file edits |
plan |
--permission-mode plan |
Read-only, must ExitPlanMode to execute |
bypassPermissions |
--dangerously-skip-permissions |
Skip all permission checks |
Root Restrictions
Claude refuses to run with --dangerously-skip-permissions when running as root (uid 0).
This is a security measure built into Claude Code. When running as root:
- The CLI outputs:
--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons - The process exits immediately without executing
This affects container environments (Docker, Daytona, E2B, etc.) which commonly run as root.
Workarounds:
- Run as a non-root user in the container
- Use
defaultpermission mode (but this requires interactive approval) - Use
acceptEditsmode for file operations (still requires Bash approval)
Headless Permission Behavior
When permissions are denied in headless mode (--print --output-format stream-json):
- Claude emits a
tool_useevent for the tool (e.g., Write, Bash) - A
userevent follows withtool_resultcontainingis_error: true - Error message:
"Claude requested permissions to X, but you haven't granted it yet." - Final
resultevent includespermission_denialsarray listing all denied tools
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Write","input":{...}}]}}
{"type":"user","message":{"content":[{"type":"tool_result","is_error":true,"content":"Claude requested permissions to write to /tmp/test.txt, but you haven't granted it yet."}]}}
{"type":"result","permission_denials":[{"tool_name":"Write","tool_use_id":"...","tool_input":{...}}]}
Subagent Types
Claude supports spawning subagents via the Task tool with subagent_type:
- Custom agents defined in config
- Built-in agents like "Explore", "Plan"
ExitPlanMode (Plan Approval)
When in plan permission mode, agent invokes ExitPlanMode tool to request execution:
interface ExitPlanModeInput {
allowedPrompts?: Array<{
tool: "Bash";
prompt: string; // e.g., "run tests"
}>;
}
This triggers a user approval event. In the universal API, this is converted to a question event with approve/reject options.
Error Handling
- Non-zero exit codes result in errors
- stderr is captured and included in error messages
- Spawn errors are caught separately
Conversion to Universal Format
Claude output is converted via convertClaudeOutput():
- If response is a string, wrap as assistant message
- If response is object with
resultfield, extract content - Parse with
ClaudeCliResponseSchemaas fallback - Extract
structured_outputas metadata if present
Model Discovery
Claude Code's /models slash command uses the standard Anthropic Models API.
API Endpoint
GET https://api.anthropic.com/v1/models?beta=true
Found by reverse engineering the CLI bundle at node_modules/@anthropic-ai/claude-code/cli.js.
API Client
The CLI contains an internal Models class with two methods:
// List all models
GET /v1/models?beta=true
// Retrieve a single model
GET /v1/models/${modelId}?beta=true
Uses this._client.getAPIList() which handles paginated responses. The ?beta=true query parameter is hardcoded to include beta/preview models.
Authentication
Uses the same Anthropic API key / OAuth credentials that Claude Code uses for conversations. The request goes to the standard Anthropic API base URL.
Hardcoded Context Window Data
The CLI also contains hardcoded output token limits for certain models (used as fallback):
{
"claude-opus-4-20250514": 8192,
"claude-opus-4-0": 8192,
"claude-opus-4-1-20250805": 8192,
// ... more entries
}
How to Replicate
Call the Anthropic API directly — no need to go through the Claude CLI:
GET https://api.anthropic.com/v1/models?beta=true
x-api-key: <ANTHROPIC_API_KEY>
anthropic-version: 2023-06-01
Command Execution & Process Management
Agent Tool Execution
The agent executes commands via the Bash tool. This is synchronous - the agent blocks until the command exits. Tool schema:
{
"command": "string",
"timeout": "number",
"workingDirectory": "string"
}
There is no background process support. If the agent needs a long-running process (e.g., dev server), it uses shell backgrounding (&) within a single Bash tool call.
User-Initiated Command Execution (! prefix)
Claude Code's TUI supports !command syntax where the user types !npm test to run a command directly. The output is injected into the conversation as a user message so the agent can see it on the next turn.
This is a client-side TUI feature only. It is not exposed in the API schema or streaming protocol. The CLI runs the command locally and stuffs the output into the next user message. There is no protocol-level concept of "user ran a command" vs "agent ran a command."
No External Command Injection API
External clients (SDKs, frontends) cannot programmatically inject command results into Claude's conversation context. The only way to provide command output to the agent is:
- Include it in the user prompt text
- Use the
!prefix in the interactive TUI
Comparison
| Capability | Supported? | Notes |
|---|---|---|
| Agent runs commands | Yes (Bash tool) |
Synchronous, blocks agent turn |
| User runs commands → agent sees output | Yes (!cmd in TUI) |
Client-side only, not in protocol |
| External API for command injection | No | |
| Background process management | No | Shell & only |
| PTY / interactive terminal | No |
Notes
- Claude CLI manages its own OAuth refresh internally
- No SDK dependency - direct CLI subprocess
- stdin is closed immediately after spawn
- Working directory is set via
cwdoption on spawn