docs/adapters/claude-local.md
The claude_local adapter runs Anthropic's Claude Code CLI locally. It supports session persistence, skills injection, and structured output parsing.
claude command available)ANTHROPIC_API_KEY set in the environment or agent config| Field | Type | Required | Description |
|---|---|---|---|
cwd | string | Yes | Working directory for the agent process (absolute path; created automatically if missing when permissions allow) |
model | string | No | Claude model to use (e.g. claude-opus-4-6) |
promptTemplate | string | No | Prompt used for all runs |
env | object | No | Environment variables (supports secret refs) |
timeoutSec | number | No | Process timeout (0 = no timeout) |
graceSec | number | No | Grace period before force-kill |
maxTurnsPerRun | number | No | Max agentic turns per heartbeat (defaults to 300) |
dangerouslySkipPermissions | boolean | No | Skip permission prompts (default: true); required for headless runs where interactive approval is impossible |
Templates support {{variable}} substitution:
| Variable | Value |
|---|---|
{{agentId}} | Agent's ID |
{{companyId}} | Company ID |
{{runId}} | Current run ID |
{{agent.name}} | Agent's name |
{{company.name}} | Company name |
The adapter persists Claude Code session IDs between heartbeats. On the next wake, it resumes the existing conversation so the agent retains full context.
Session resume is cwd-aware: if the agent's working directory changed since the last run, a fresh session starts instead.
If resume fails with an unknown session error, the adapter automatically retries with a fresh session.
previous_message_id (recovery)Symptom in logs / issue thread:
API Error: 400 diagnostics.previous_message_id: must be the `id` from a prior /v1/messages response (starts with `msg_`)
What it means: the on-disk Claude Code transcript JSONL for that session contains a malformed (non-msg_-prefixed) previous_message_id. Anthropic's /v1/messages rejects every resume attempt against that transcript with a deterministic 400. Without guards, Paperclip would re-persist the same poisoned session id and the issue is stranded permanently — see RED-976 / RED-978.
What the adapter does automatically:
--resume attempt returns this 400, the adapter retries once with a fresh session, deletes the poisoned <session>.jsonl from the local Claude config dir (best effort), and uses the fresh session id going forward.session_id written back to the task session store, even if Claude Code emits one in the result event. The adapter returns sessionId: null, sessionParams: null, and errorCode: "claude_poisoned_previous_message_id".clearSession: true on the result, which causes the heartbeat service to drop any persisted session row for that issue (clearTaskSessions). The next continuation starts from a clean slate.On-call checklist if you see this in production:
errorCode is claude_poisoned_previous_message_id in the run row — that means the guards fired correctly and the issue auto-recovers on the next heartbeat.agentTaskSessions for that (agentId, taskKey) was cleared. If not, the adapter return value was lost (e.g. a malformed run finalization) — escalate; do not manually edit the row, file a child issue with the run id.clearSession: true is authoritative regardless of remote disk state.The adapter creates a temporary directory with symlinks to Paperclip skills and passes it via --add-dir. This makes skills discoverable without polluting the agent's working directory.
For manual local CLI usage outside heartbeat runs (for example running as claudecoder directly), use:
pnpm paperclipai agent local-cli claudecoder --company-id <company-id>
This installs Paperclip skills in ~/.claude/skills, creates an agent API key, and prints shell exports to run as that agent.
Use the "Test Environment" button in the UI to validate the adapter config. It checks:
ANTHROPIC_API_KEY vs subscription login)claude --print - --output-format stream-json --verbose with prompt Respond with hello.) to verify CLI readiness