# `ClaudeAgentSDK.Client`
[🔗](https://github.com/nshkrdotcom/claude_agent_sdk/blob/v0.17.2/lib/claude_agent_sdk/client.ex#L1)

Bidirectional client for Claude Code with hooks support.

This GenServer maintains a persistent connection to the Claude CLI process,
handles control protocol messages, and invokes hook callbacks.

The Client enables:
- Bidirectional streaming communication
- Runtime hook callback invocation
- Control protocol request/response handling
- Message queueing and delivery

## Usage

    # Define hook callbacks
    def check_bash(input, _tool_use_id, _context) do
      if dangerous?(input), do: Output.deny("Blocked"), else: Output.allow()
    end

    # Configure options with hooks
    options = %Options{
      allowed_tools: ["Bash", "Write"],
      hooks: %{
        pre_tool_use: [
          Matcher.new("Bash", [&check_bash/3])
        ]
      }
    }

    # Start client
    {:ok, pid} = Client.start_link(options)

    # Send query
    Client.send_message(pid, "Run: echo 'Hello'")

    # Receive messages
    stream = Client.stream_messages(pid)
    Enum.each(stream, &IO.inspect/1)

    # Stop client
    Client.stop(pid)

## With Streaming

    {:ok, pid} = Client.start_link(options)

    # Start listening in separate process
    task = Task.async(fn ->
      Client.stream_messages(pid)
      |> Enum.take_while(&(&1.type != :result))
      |> Enum.to_list()
    end)

    # Send message
    Client.send_message(pid, "Write a function")

    # Wait for completion
    messages = Task.await(task, :infinity)

See: https://docs.anthropic.com/en/docs/claude-code/sdk

## Architecture Note

`ClaudeAgentSDK.Client` remains SDK-local because it owns the advanced control
protocol family: hooks, permission callbacks, SDK MCP routing, and related
request/response state. The shared core now owns the control-session lifecycle
through `CliSubprocessCore.ProtocolSession`; this module stays above that
boundary and keeps only Claude-specific control semantics.

`agent_session_manager` may optionally bridge into this module through
`ASM.Extensions.ProviderSDK.Claude`, but the bridge does not redefine these
control APIs. Runtime control ownership remains here.

# `pending_request_entry`

```elixir
@type pending_request_entry() ::
  {:initialize, reference()}
  | {:set_model, GenServer.from(), String.t(), reference()}
  | {:set_permission_mode, GenServer.from(), atom(), reference()}
  | {:interrupt, GenServer.from(), reference()}
  | {:rewind_files, GenServer.from(), reference()}
  | {:mcp_status, GenServer.from(), reference()}
```

# `state`

```elixir
@type state() :: %ClaudeAgentSDK.Client{
  accumulated_text: String.t(),
  active_subscriber: reference() | nil,
  control_request_timeout_ms: pos_integer(),
  hook_callback_timeouts: %{required(String.t()) =&gt; pos_integer()},
  init_request_id: String.t() | nil,
  init_timeout: {reference(), pos_integer()} | nil,
  init_waiters: [GenServer.from()],
  initialized: boolean(),
  initialized_waiters: [GenServer.from()],
  options: ClaudeAgentSDK.Options.t(),
  pending_callbacks: %{
    required(String.t()) =&gt; %{
      pid: pid(),
      monitor_ref: reference(),
      signal: ClaudeAgentSDK.AbortSignal.t(),
      type: :hook | :permission
    }
  },
  pending_inbound: :queue.queue(),
  pending_inbound_dropped: non_neg_integer(),
  pending_inbound_size: non_neg_integer(),
  pending_outbound: :queue.queue(binary()),
  pending_permission_change: String.t() | nil,
  pending_requests: %{required(String.t()) =&gt; pending_request_entry()},
  permission_bridge: :ets.tid() | nil,
  protocol_session: pid() | nil,
  protocol_session_monitor_ref: reference() | nil,
  registry: ClaudeAgentSDK.Hooks.Registry.t(),
  sdk_mcp_servers: %{required(String.t()) =&gt; pid()},
  server_info: map() | nil,
  session_id: term(),
  stderr_buffer: String.t(),
  stream_buffer_limit: non_neg_integer(),
  stream_stop_reason: String.t() | nil,
  subscriber_monitors: %{required(reference()) =&gt; reference()},
  subscriber_queue: [{reference(), String.t()}],
  subscribers: %{required(reference()) =&gt; pid()}
}
```

Client state.

Fields:
- `protocol_session` - Shared core-owned control session
- `options` - Configuration options
- `registry` - Hook callback registry
- `hook_callback_timeouts` - Map of callback_id => timeout_ms
- `subscribers` - Map of ref => pid for streaming subscriptions
- `subscriber_monitors` - Map of ref => monitor_ref for subscriber lifecycle cleanup
- `pending_requests` - Map of request_id => request task tracking entries
- `pending_outbound` - FIFO queue of outbound payloads accepted before initialize completes
- `pending_callbacks` - Map of request_id => %{pid, monitor_ref, signal, type} for in-flight control callbacks
- `initialized` - Whether initialization handshake completed
- `sdk_mcp_servers` - Map of server_name => registry_pid for SDK MCP servers
- `accumulated_text` - Buffer for partial text (streaming, v0.6.0)
- `active_subscriber` - Current streaming consumer reference (v0.6.0)
- `subscriber_queue` - Pending message queue (v0.6.0)
- `init_waiters` - Callers waiting for initialize request send
- `initialized_waiters` - Callers waiting for initialize completion
- `pending_inbound` - Buffered inbound events/messages before first subscriber
- `pending_inbound_size` - Number of buffered inbound entries before first subscriber
- `pending_inbound_dropped` - Dropped inbound entries due to buffer limit
- `stream_buffer_limit` - Max buffered inbound entries before first subscriber
- `control_request_timeout_ms` - Per-client timeout for control requests in milliseconds

# `await_init_sent`

```elixir
@spec await_init_sent(pid(), pos_integer() | nil) ::
  {:ok, String.t()} | {:error, term()}
```

Waits until the initialize request has been sent to the transport.

Returns `{:ok, request_id}` once the initialize request is sent, or
`{:error, reason}` if the client is not alive or times out.

# `await_initialized`

```elixir
@spec await_initialized(pid(), pos_integer() | nil) :: :ok | {:error, term()}
```

Waits for the client to finish initialization.

Returns `:ok` once the initialize handshake completes, or `{:error, reason}`
if the client is not alive or times out.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `get_agent`

```elixir
@spec get_agent(pid()) :: {:ok, atom()} | {:error, term()}
```

Gets the currently active agent.

## Parameters

  * `client` - The client PID

## Returns

`{:ok, agent_name}` or `{:error, reason}`

## Examples

    {:ok, :coder} = Client.get_agent(client)

# `get_available_agents`

```elixir
@spec get_available_agents(pid()) :: {:ok, [atom()]} | {:error, term()}
```

Gets the list of available agent names.

## Parameters

  * `client` - The client PID

## Returns

`{:ok, [agent_name]}` or `{:error, reason}`

## Examples

    {:ok, [:coder, :researcher]} = Client.get_available_agents(client)

# `get_mcp_status`

```elixir
@spec get_mcp_status(pid()) :: {:ok, map()} | {:error, term()}
```

Gets current MCP server connection status.

Returns a map with `"mcpServers"` key containing a list of server status
objects, each with:
- `"name"` - Server name
- `"status"` - Connection status: `"connected"`, `"pending"`, `"failed"`,
  `"needs-auth"`, `"disabled"`

## Parameters

  * `client` - The client PID

## Returns

`{:ok, map()}` or `{:error, reason}`

## Examples

    {:ok, status} = Client.get_mcp_status(client)
    for server <- status["mcpServers"] do
      IO.puts("#{server["name"]}: #{server["status"]}")
    end

# `get_model`

```elixir
@spec get_model(pid()) :: {:ok, String.t()} | {:error, :model_not_set}
```

Retrieves the currently active model name.

# `get_server_info`

```elixir
@spec get_server_info(pid()) :: {:ok, map()} | {:error, term()}
```

Returns the server initialization info provided by the CLI.

# `interrupt`

```elixir
@spec interrupt(pid()) :: :ok | {:error, term()}
```

Sends an interrupt control request to the CLI.

# `query`

```elixir
@spec query(pid(), String.t() | Enumerable.t(), String.t()) :: :ok | {:error, term()}
```

Sends a request in streaming mode, injecting `session_id` when missing.

Matches Python SDK behavior:
- String prompts are wrapped as a `"user"` message with `parent_tool_use_id: nil`
- Map prompts (or enumerables of maps) get `session_id` injected if absent

# `receive_response`

```elixir
@spec receive_response(pid()) ::
  {:ok, [ClaudeAgentSDK.Message.t()]} | {:error, term()}
```

Collects messages until a result frame is received.

Useful for workflows that only care about a single response and want
to avoid managing streaming state manually.

# `receive_response_stream`

```elixir
@spec receive_response_stream(pid()) :: Enumerable.t(ClaudeAgentSDK.Message.t())
```

Streams messages until a result frame is received.

This provides a streaming equivalent of `receive_response/1`.

# `rewind_files`

```elixir
@spec rewind_files(pid(), String.t()) :: :ok | {:error, term()}
```

Rewinds tracked files to their state at a specific user message.

Requires `Options.enable_file_checkpointing` to be enabled when starting the client.

# `send_message`

```elixir
@spec send_message(pid(), String.t() | map()) :: :ok | {:error, term()}
```

Sends a message to Claude.

In streaming mode, this queues the message for sending.

## Parameters

- `client` - Client PID
- `message` - Message string or map

## Returns

`:ok` or `{:error, reason}`

## Examples

    Client.send_message(pid, "Write a hello world function")

# `set_agent`

```elixir
@spec set_agent(pid(), atom()) :: :ok | {:error, term()}
```

Switches to a different agent configuration.

## Parameters

  * `client` - The client PID
  * `agent_name` - The name of the agent to switch to (atom)

## Returns

`:ok` or `{:error, reason}`

## Examples

    Client.set_agent(client, :researcher)

# `set_model`

```elixir
@spec set_model(pid(), String.t()) :: :ok | {:error, term()}
```

Requests a runtime model switch.

Returns `:ok` when the CLI confirms the change or `{:error, reason}`
when validation fails or the CLI rejects the request.

# `set_permission_mode`

```elixir
@spec set_permission_mode(pid(), ClaudeAgentSDK.Permission.permission_mode()) ::
  :ok | {:error, :invalid_permission_mode}
```

Sets the permission mode at runtime.

Changes how tool permissions are handled for subsequent tool uses.

## Parameters

- `client` - Client PID
- `mode` - Permission mode atom (`:default`, `:accept_edits`, `:plan`, `:bypass_permissions`, `:auto`, `:dont_ask`)

## Returns

- `:ok` - Successfully changed mode
- `{:error, :invalid_permission_mode}` - Invalid mode provided

## Examples

    Client.set_permission_mode(pid, :plan)
    Client.set_permission_mode(pid, :accept_edits)
    Client.set_permission_mode(pid, :bypass_permissions)
    Client.set_permission_mode(pid, :auto)

# `start_link`

```elixir
@spec start_link(
  ClaudeAgentSDK.Options.t(),
  keyword()
) :: GenServer.on_start()
```

Starts the client GenServer.

Validates hooks configuration, starts Claude CLI process, and performs
initialization handshake.

## Parameters

- `options` - ClaudeAgentSDK.Options struct with hooks configuration
- `opts` - Optional runtime overrides (e.g. `:execution_surface`,
  `:control_request_timeout_ms`)

## Returns

- `{:ok, pid}` - Successfully started
- `{:error, reason}` - Failed to start

## Examples

    options = %Options{
      hooks: %{
        pre_tool_use: [Matcher.new("Bash", [&my_hook/3])]
      }
    }

    {:ok, pid} = Client.start_link(options)

# `stop`

```elixir
@spec stop(pid()) :: :ok
```

Stops the client.

Terminates the CLI process and cleans up resources.

## Parameters

- `client` - Client PID

## Returns

`:ok`

## Examples

    Client.stop(pid)

# `stream_messages`

```elixir
@spec stream_messages(pid()) :: Enumerable.t(ClaudeAgentSDK.Message.t())
```

Returns a stream of messages from Claude.

Subscribes to the client and yields messages as they arrive.

## Parameters

- `client` - Client PID

## Returns

Enumerable stream of Message structs

## Examples

    Client.stream_messages(pid)
    |> Stream.filter(&(&1.type == :assistant))
    |> Enum.to_list()

# `subscribe`

```elixir
@spec subscribe(pid()) :: {pid(), reference() | nil}
```

Subscribes to the client's message stream and returns a subscription reference.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
