Packages
claude_code
0.36.4
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.1
0.32.2
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Claude Agent SDK for Elixir – Build AI agents with Claude Code
Current section
Files
Jump to
Current section
Files
lib/claude_code/cli/control.ex
defmodule ClaudeCode.CLI.Control do
@moduledoc """
Bidirectional control protocol for the Claude CLI.
Builds and classifies control messages that share the stdin/stdout
transport with regular SDK messages. Part of the CLI protocol layer.
## Message Classification
All messages from the CLI are classified into one of three categories:
* `{:control_request, msg}` — The CLI is requesting something from the SDK
* `{:control_response, msg}` — The CLI is responding to a previous SDK request
* `{:message, msg}` — A regular SDK message (system, assistant, user, result, etc.)
## Outbound Requests (SDK -> CLI)
Request builders produce single-line JSON strings suitable for writing
to the CLI's stdin:
* `initialize_request/3` — Initialize the control protocol
* `set_model_request/2` — Change the active model
* `set_permission_mode_request/2` — Change the permission mode
* `rewind_files_request/2` — Rewind files to a previous state
* `mcp_status_request/1` — Query MCP server status
* `mcp_reconnect_request/2` — Reconnect a failed MCP server
* `mcp_toggle_request/3` — Enable or disable an MCP server
* `mcp_set_servers_request/2` — Replace dynamic MCP servers
* `stop_task_request/2` — Stop a running task
## Response Builders (SDK -> CLI)
When the CLI sends a control request, the SDK responds with:
* `success_response/2` — Successful response with data
* `error_response/2` — Error response with message
## Response Parsing (CLI -> SDK)
When the CLI responds to an SDK request:
* `parse_control_response/1` — Parse into tagged tuples
"""
# --- Classification ---------------------------------------------------------
@doc """
Classifies a decoded JSON message as a control or regular message.
## Examples
iex> ClaudeCode.CLI.Control.classify(%{"type" => "control_request", "request_id" => "req_1"})
{:control_request, %{"type" => "control_request", "request_id" => "req_1"}}
iex> ClaudeCode.CLI.Control.classify(%{"type" => "assistant", "message" => %{}})
{:message, %{"type" => "assistant", "message" => %{}}}
"""
@spec classify(map()) ::
{:control_request, map()} | {:control_response, map()} | {:control_cancel, map()} | {:message, map()}
def classify(%{"type" => "control_request"} = msg), do: {:control_request, msg}
def classify(%{"type" => "control_response"} = msg), do: {:control_response, msg}
def classify(%{"type" => "control_cancel_request"} = msg), do: {:control_cancel, msg}
def classify(msg), do: {:message, msg}
# --- Request ID Generation --------------------------------------------------
@doc """
Generates a unique request ID with the given counter prefix.
The format is `req_{counter}_{hex}` where `hex` is 8 random hex characters.
## Examples
iex> id = ClaudeCode.CLI.Control.generate_request_id(0)
iex> String.starts_with?(id, "req_0_")
true
"""
@spec generate_request_id(non_neg_integer()) :: String.t()
def generate_request_id(counter) do
hex = 4 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower)
"req_#{counter}_#{hex}"
end
# --- Outbound Request Builders (SDK -> CLI) ---------------------------------
@doc """
Builds an initialize control request JSON string.
## Parameters
* `request_id` - Unique request identifier
* `hooks` - Optional hook configurations (map or nil)
* `agents` - Optional agent configurations (map or nil)
* `sdk_mcp_servers` - Optional list of SDK MCP server name strings (nil to omit)
"""
@spec initialize_request(String.t(), map() | nil, map() | nil, [String.t()] | nil, keyword()) :: String.t()
def initialize_request(request_id, hooks \\ nil, agents \\ nil, sdk_mcp_servers \\ nil, extra_opts \\ []) do
request =
%{subtype: "initialize"}
|> maybe_put(:hooks, hooks)
|> maybe_put(:agents, agents)
|> maybe_put(:sdkMcpServers, sdk_mcp_servers)
|> maybe_put(:promptSuggestions, Keyword.get(extra_opts, :prompt_suggestions))
|> maybe_put(:toolConfig, Keyword.get(extra_opts, :tool_config))
encode_control_request(request_id, request)
end
@doc """
Builds a set_model control request JSON string.
## Parameters
* `request_id` - Unique request identifier
* `model` - The model identifier string
"""
@spec set_model_request(String.t(), String.t()) :: String.t()
def set_model_request(request_id, model) do
encode_control_request(request_id, %{subtype: "set_model", model: model})
end
@doc """
Builds a set_permission_mode control request JSON string.
## Parameters
* `request_id` - Unique request identifier
* `mode` - The permission mode string (e.g., "bypassPermissions")
"""
@spec set_permission_mode_request(String.t(), String.t()) :: String.t()
def set_permission_mode_request(request_id, mode) do
encode_control_request(request_id, %{subtype: "set_permission_mode", permission_mode: mode})
end
@doc """
Builds a rewind_files control request JSON string.
## Parameters
* `request_id` - Unique request identifier
* `user_message_id` - The user message ID to rewind to
* `opts` - Optional keyword list:
* `:dry_run` - When `true`, preview changes without applying them
"""
@spec rewind_files_request(String.t(), String.t(), keyword()) :: String.t()
def rewind_files_request(request_id, user_message_id, opts \\ []) do
%{subtype: "rewind_files", user_message_id: user_message_id}
|> maybe_put(:dryRun, Keyword.get(opts, :dry_run))
|> then(&encode_control_request(request_id, &1))
end
@doc """
Builds an mcp_status control request JSON string.
## Parameters
* `request_id` - Unique request identifier
"""
@spec mcp_status_request(String.t()) :: String.t()
def mcp_status_request(request_id) do
encode_control_request(request_id, %{subtype: "mcp_status"})
end
@doc """
Builds an interrupt control request JSON string.
Interrupt is fire-and-forget — the CLI stops generating and emits a result message.
## Parameters
* `request_id` - Unique request identifier
"""
@spec interrupt_request(String.t()) :: String.t()
def interrupt_request(request_id) do
encode_control_request(request_id, %{subtype: "interrupt"})
end
@doc """
Builds an mcp_reconnect control request JSON string.
Reconnects a disconnected or failed MCP server.
## Parameters
* `request_id` - Unique request identifier
* `server_name` - The name of the MCP server to reconnect
"""
@spec mcp_reconnect_request(String.t(), String.t()) :: String.t()
def mcp_reconnect_request(request_id, server_name) do
encode_control_request(request_id, %{subtype: "mcp_reconnect", serverName: server_name})
end
@doc """
Builds an mcp_toggle control request JSON string.
Enables or disables an MCP server.
## Parameters
* `request_id` - Unique request identifier
* `server_name` - The name of the MCP server to toggle
* `enabled` - Whether the server should be enabled
"""
@spec mcp_toggle_request(String.t(), String.t(), boolean()) :: String.t()
def mcp_toggle_request(request_id, server_name, enabled) do
encode_control_request(request_id, %{subtype: "mcp_toggle", serverName: server_name, enabled: enabled})
end
@doc """
Builds an mcp_set_servers control request JSON string.
Replaces the set of dynamically managed MCP servers.
## Parameters
* `request_id` - Unique request identifier
* `servers` - Map of server name to server config (stdio, sse, http)
"""
@spec mcp_set_servers_request(String.t(), map()) :: String.t()
def mcp_set_servers_request(request_id, servers) do
encode_control_request(request_id, %{subtype: "mcp_set_servers", servers: servers})
end
@doc """
Builds a stop_task control request JSON string.
Stops a running task. A task_notification with status 'stopped' will be emitted.
## Parameters
* `request_id` - Unique request identifier
* `task_id` - The task ID from task_notification events
"""
@spec stop_task_request(String.t(), String.t()) :: String.t()
def stop_task_request(request_id, task_id) do
encode_control_request(request_id, %{subtype: "stop_task", task_id: task_id})
end
# --- Response Builders (SDK -> CLI, answering CLI requests) -----------------
@doc """
Builds a success control response JSON string.
Used to respond to a control request from the CLI with a successful result.
## Parameters
* `request_id` - The request ID being responded to
* `response_data` - The response payload (map)
"""
@spec success_response(String.t(), map()) :: String.t()
def success_response(request_id, response_data) do
Jason.encode!(%{
type: "control_response",
response: %{subtype: "success", request_id: request_id, response: response_data}
})
end
@doc """
Builds an error control response JSON string.
Used to respond to a control request from the CLI with an error.
## Parameters
* `request_id` - The request ID being responded to
* `error_message` - The error description string
"""
@spec error_response(String.t(), String.t()) :: String.t()
def error_response(request_id, error_message) do
Jason.encode!(%{
type: "control_response",
response: %{subtype: "error", request_id: request_id, error: error_message}
})
end
# --- Response Parsing (CLI -> SDK) ------------------------------------------
@doc """
Parses a control response message from the CLI.
Returns tagged tuples indicating success or error:
* `{:ok, request_id, response_data}` — Successful response
* `{:error, request_id, error_message}` — Error response or parse failure
## Examples
iex> msg = %{"type" => "control_response", "response" => %{"subtype" => "success", "request_id" => "req_1", "response" => %{}}}
iex> ClaudeCode.CLI.Control.parse_control_response(msg)
{:ok, "req_1", %{}}
"""
@spec parse_control_response(map()) :: {:ok, String.t(), map()} | {:error, String.t() | nil, String.t()}
def parse_control_response(%{"response" => %{"subtype" => "success", "request_id" => req_id, "response" => data}}) do
{:ok, req_id, data}
end
def parse_control_response(%{"response" => %{"subtype" => "success", "request_id" => req_id}}) do
{:ok, req_id, %{}}
end
def parse_control_response(%{"response" => %{"subtype" => "error", "request_id" => req_id, "error" => error}}) do
{:error, req_id, error}
end
def parse_control_response(%{"response" => %{"subtype" => subtype, "request_id" => req_id}}) do
{:error, req_id, "Unknown control response subtype: #{subtype}"}
end
def parse_control_response(_) do
{:error, nil, "Invalid control response: missing response field"}
end
# --- Private ----------------------------------------------------------------
defp encode_control_request(request_id, request) do
Jason.encode!(%{type: "control_request", request_id: request_id, request: request})
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end