New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@kodizm/acp

Package Overview
Dependencies
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@kodizm/acp

Custom ACP server bridging Claude Code, codex, and opencode CLIs through a single Kodizm-flavored protocol surface.

latest
Source
npmnpm
Version
0.6.9
Version published
Weekly downloads
1.7K
-34.36%
Maintainers
1
Weekly downloads
 
Created
Source

kodizm-acp

ACP bridge that drives the Claude Code and opencode CLIs through one canonical wire.

What it is

@kodizm/acp is the Kodizm runtime's Agent Client Protocol bridge. It speaks one canonical JSON-RPC surface to the orchestrator, then translates each turn down to whichever CLI backend the session was opened against. The orchestrator never branches on backend; the same session/new, session/prompt, and sessionUpdate shapes carry every feature across all three.

Two CLIs are supported: Claude Code (via @anthropic-ai/claude-agent-sdk) as the primary backend, and opencode (via createOpencodeServer from @opencode-ai/sdk) as the secondary one.

How it works

+------------------+    JSON-RPC over    +------------------+    native protocol    +-----------+
|   Orchestrator   | <----- NDJSON ----> |    AcpServer     | <-------------------> |  Backend  |
|   (Kodizm core)  |     (stdio/pipe)    |  + BackendDriver |    (SDK / subproc)    |    CLI    |
+------------------+                     +------------------+                       +-----------+
  • KODIZM_BACKEND selects the driver at process boot. One process per backend.
  • AcpServer validates every inbound request against the canonical schema, then routes to the BackendDriver interface.
  • Each driver maps the canonical request to its CLI's native shape. Stream events flow back through emit.send() and surface as sessionUpdate notifications on the wire.

The driver contract is a single TypeScript interface with seven methods. The server never imports any concrete driver. New backends extend the registry; the wire layer does not change.

Backend support

FeatureClaudeopencode
session/new, session/prompt, session/cancelyesyes
session/load (resume)yesyes
session/forkyesyes
session/compact (manual)yesyes
Image content blocksyesyes
Token + cost rollup (usage event)yesyes
additionalDirectories (sandbox)yesn/a
mcpServers injectionyesyes
systemPrompt replace + appendyesyes
skills pre-loadyesn/a
Permissions (permission_request)yesyes
askUserQuestionyesyes
Subagent eventsyesyes
Thinking eventsyesyes
Cross-process Pattern B resumeyesyes
Debug captureyesyes

[!NOTE] The published bin (kodizm-acp from dist/index.js) wires both backends: set KODIZM_BACKEND to claude or opencode and the bin resolves the matching driver itself.

Install

bun add @kodizm/acp

Requires Bun >= 1.1.0. The opencode CLI must be installed separately when you use that backend.

Quick start

The bin runs over stdio:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}}' \
  | KODIZM_BACKEND=claude \
    CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat01-..." \
    CLAUDE_CODE_REMOTE=1 \
    bunx @kodizm/acp

Replace the credential pair with ANTHROPIC_API_KEY=... for the api-key path.

Configuration

Environment variables

VariableRequiredDescription
KODIZM_BACKENDyesclaude / opencode
CLAUDE_CODE_OAUTH_TOKEN + CLAUDE_CODE_REMOTE=1claude (sub)Subscription auth
ANTHROPIC_API_KEYclaude (api)API key auth
CLAUDE_CODE_PATHoptionalPath to claude binary (default /usr/local/bin/claude)
OPENCODE_AUTH_CONTENTopencode (env)JSON keyed by providerID. Layered onto subprocess env for the bridge lifetime only. Without it, opencode reads ~/.local/share/opencode/auth.json
KODIZM_LOG_LEVELoptionaldebug / info / warn / error. Default info
KODIZM_DEBUGoptional1 enables process-wide debug capture
KODIZM_DEBUG_DIRoptionalForensic JSONL dir, default /tmp/kodizm-debug
KODIZM_DEBUG_RAW_SECRETSincident-only1 disables redaction. Never set in production
KODIZM_ACP_FORWARD_STDERRoptional1 tees a spawned backend subprocess's stderr to parent stderr

Stdout is reserved for ACP frames. Logs go to stderr.

Session options (NewSessionRequest)

type NewSessionRequest = {
  cwd: string                        // absolute path
  mcpServers: McpServer[]
  additionalDirectories?: string[]   // absolute paths
  systemPrompt?: string | { append: string }
  model?: string                     // e.g. 'claude-haiku-4-5-20251001'
  skills?: string[]                  // claude only
  toolPolicy?: ToolPolicy
  autoCompact?: boolean
  permissionTimeoutMs?: number       // mutually exclusive with permissionDeferTimeoutMs
  permissionDeferTimeoutMs?: number
  debug?: boolean
  debugCaptureRawSdk?: boolean
  debugCaptureRpc?: boolean
  heartbeatIntervalMs?: number
  inactivityThresholdMs?: number
  settingSources?: ('user' | 'project' | 'local')[]  // claude only; opt-out
  _meta?: Record<string, unknown>    // passthrough; canonical fields rejected
}

[!WARNING] permissionTimeoutMs and permissionDeferTimeoutMs are mutually exclusive. Pick hard-deny on timeout OR soft-defer on timeout, not both. The schema rejects the conflict with a clear error.

[!NOTE] settingSources is the claude-only opt-out for filesystem config layering. When omitted the SDK's own default fires, matching the standalone Claude Code CLI: project CLAUDE.md + .claude/CLAUDE.md + .claude/rules/*.md load from cwd walking up; user ~/.claude/CLAUDE.md + ~/.claude/rules/*.md and CLAUDE.local.md load too. Pass [] to disable every fs scope; pass a selective subset like ['project'] to load only project-tracked files. Opencode (AGENTS.md / CLAUDE.md / CONTEXT.md from session directory) has no parallel field and ignores this option.

Tool policy

type ToolPolicy = {
  defaultMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
  allow?: string[]   // e.g. ['Read', 'Bash:git status', 'mcp:server/tool']
  deny?: string[]
  ask?: string[]
}

The parser lives in src/wire/policy.ts. Each backend translates the canonical pattern grammar to its native rule shape.

MCP servers

type McpServer = {
  type: 'http'
  name: string
  url: string
  headers?: { name: string; value: string }[]
}

The http transport ships today. Opencode adds servers via sdk.mcp.add per session.

API reference

BackendDriver

Every backend implements this contract:

interface BackendDriver {
  capabilities(): DriverCapabilities
  initialize(params: InitializeRequest): Promise<InitializeResult>
  newSession(params: NewSessionRequest): Promise<NewSessionResult>
  prompt(sessionId: string, params: PromptRequest, emit: EventEmitter): Promise<PromptResult>
  cancel(request: CancelRequest): Promise<void>
  loadSession(params: LoadSessionRequest): Promise<NewSessionResult>
  forkSession(params: ForkSessionRequest): Promise<NewSessionResult>
  compact(request: CompactSessionRequest, emit: EventEmitter): Promise<void>
}

interface DriverCapabilities {
  resume: boolean
  fork: boolean
  fileUpload: boolean
  thinking: boolean
  subagent: boolean
  skillEvents: boolean
  debug: boolean
  askQuestion: boolean
}

Capability gating runs at the dispatcher: loadSession rejects with MethodNotSupportedError (-32601) when resume is false; forkSession rejects when fork is false. The other six flags are advisory metadata for the orchestrator.

Public exports

The bin (src/index.ts) is the only public surface. Importers get the runtime helpers, not the drivers themselves:

ExportPurpose
SupportedBackend'claude' | 'opencode'
BackendNotConfiguredError, UnknownBackendErrorstartup-time errors
resolveBackendFromEnv(env)parse KODIZM_BACKEND from a captured env
installShutdownHook()wire SIGTERM + SIGINT to graceful shutdown
performShutdown(graceMs?)run the shutdown side-effects manually
registerActiveRecorder(r)track a DebugRecorder for shutdown flush
registerShutdownFlusher(fn)register the transport flush callback
SHUTDOWN_GRACE_MSdefault 3s grace budget
main()the bin's entry function

Drivers, wire types, and createAcpServer are internal. To embed programmatically, import them directly from their module paths under src/.

Programmatic embedding

import { ClaudeDriver } from '@kodizm/acp/src/backends/claude/driver.ts'
import { createAcpServer } from '@kodizm/acp/src/server/acp-server.ts'
import { createNdjsonTransport } from '@kodizm/acp/src/server/transport.ts'
import { query } from '@anthropic-ai/claude-agent-sdk'

const driver = new ClaudeDriver({
  credentials: { type: 'subscription', token: process.env.CLAUDE_CODE_OAUTH_TOKEN! },
  agentInfo: { version: '0.5.4' },
  sdk: { query: ({ prompt, options }) => query({ prompt, options }) },
})

const server = createAcpServer({
  transport: createNdjsonTransport({ readable, writable }),
  backend: driver,
})

await server.serve()

The opencode driver follows the same shape; it takes no spawn factory, since the SDK helper handles the subprocess.

Wire reference

JSON-RPC methods (inbound)

MethodPurpose
initializehandshake; returns protocolVersion + agentInfo + capabilities
session/newopen a session; returns { sessionId }
session/promptdrive a turn; streams sessionUpdate notifications, returns PromptResult
session/cancelabort the in-flight prompt for a session
session/loadresume a prior session by id (gated on resume)
session/forkbranch a session with optional overrides (gated on fork)
session/compacttrigger manual context compaction

PromptResult.stopReason is one of: end_turn, cancelled, process_died, max_tokens, tool_use, session_failed. When session_failed, the result also carries failureReason and failureDetail.

sessionUpdate events

The SessionUpdateEvent discriminated union has 21 variants:

TypeWhen
output_chunkstreaming model output
thinking_chunkstreaming reasoning
tool_call_begin / progress / endtool lifecycle (one begin + one end per call)
permission_requestmodel wants to run a gated tool
permission_deferred / permission_resumedPattern B lifecycle
question_requestmodel asks the user a question
usagetoken + cost rollup
subagent_spawn / subagent_completenested agent lifecycle
skill_activationa skill loaded mid-turn (claude only)
model_advertisementthe actual model the backend chose
process_diedsubprocess crash (opencode)
cancelledturn was cancelled
compaction_started / compaction_completedcontext compaction; trigger: 'manual' | 'auto'
debug_logone of 10 stages: rpc.in, rpc.out, sdk.message, sdk.error, tool.permission_request, tool.permission_response, session.config, driver.state_change, transport.spawn, transport.exit
heartbeatperiodic liveness signal
session_failedstructured lifecycle failure

Outbound RPCs (server to orchestrator)

MethodWhen
sessionUpdateevery event above flows as a notification
session/request_permissionorchestrator decides on a permission_request
session/ask_user_questionorchestrator answers a question_request
session/permission_deferred_persistPattern B write fallback when no deferredStore
session/permission_deferred_statePattern B read fallback when no deferredStore

The permission and ask-question RPC names have legacy aliases (requestPermission, askUserQuestion); both forms route to the same handler.

Cross-process resume (Pattern B)

A driver instance can resume a session that an earlier process started. Useful when a container restart, deploy, or crash interrupts an active turn.

  • claude: standard session/load against the SDK's resume mode; the JSONL transcript on disk is authoritative.
  • opencode: OpencodeDriver.loadSession({ sessionId, _meta: { opencodeSessionId } }); opencode's SQLite persistence survives process death.

Deferred permissions (the orchestrator decided to "ask later" on a tool gate) are persisted via either an injected DeferredPermissionStore or the session/permission_deferred_persist outbound RPC. The next process picks up where the prior one left off and emits permission_resumed.

Failure handling

session_failed carries one of seven reasons:

ReasonContainer action
sdk_stallexit
transport_errorexit
internal_panicexit
protocol_violationexit
sdk_throwstay alive (orchestrator may retry)
auth_errorstay alive (orchestrator can refresh credentials)
rate_limitstay alive (orchestrator backs off)

The exit decision lives in src/util/exit-policy.ts. The bin consults it after a session_failed and triggers graceful shutdown when true.

Development

bun install
bun test test/unit                       # mocked; ~600 tests
bun test test/e2e                        # full ACP roundtrip
bun test test/integration                # real-API smokes (requires creds)
bunx tsc --noEmit                        # typecheck
bunx biome check --write src test        # lint + format
bun run build                            # compile to dist/index.js

The integration suite gates each backend on its own auth probe; tests skip cleanly when credentials are not present.

License

Apache-2.0. See LICENSE.

Keywords

acp

FAQs

Package last updated on 23 Sep 2026

Related posts