PHP wrapper for Claude Code. Fun fact: This SDK was built by using Claude Code to port its own Python SDK to PHP. 🤖
Stream Claude's responses, track token usage, hook into events, and integrate AI coding assistance into your PHP apps.
composer require helgesverre/claude-code-sdkMake sure Claude Code CLI is installed:
npm install -g @anthropic-ai/claude-code<?php
use HelgeSverre\ClaudeCode\ClaudeCode;
use HelgeSverre\ClaudeCode\Types\Messages\AssistantMessage;
use HelgeSverre\ClaudeCode\Types\Messages\SystemMessage;
use HelgeSverre\ClaudeCode\Types\Messages\ResultMessage;
use HelgeSverre\ClaudeCode\Types\ContentBlocks\TextBlock;
// Send a query to Claude
$messages = ClaudeCode::query("What files are in the current directory?");
// Process the streaming response
foreach ($messages as $message) {
match (true) {
$message instanceof SystemMessage =>
echo "[SYSTEM] {$message->subtype}\n",
$message instanceof AssistantMessage =>
array_map(function ($block) {
if ($block instanceof TextBlock) {
echo "[CLAUDE] {$block->text}\n";
}
}, $message->content),
$message instanceof ResultMessage =>
echo "[DONE] Cost: \${$message->totalCostUsd} | Time: {$message->durationMs}ms\n",
default => null
};
}use HelgeSverre\ClaudeCode\ClaudeCode;
use HelgeSverre\ClaudeCode\Types\Config\Options;
use HelgeSverre\ClaudeCode\Types\Enums\PermissionMode;
$options = new Options(
systemPrompt: "You are a helpful coding assistant",
allowedTools: ['Read', 'Write', 'Edit'],
permissionMode: PermissionMode::acceptEdits,
maxTurns: 5,
);
/**
* A generator is returned, allowing you to stream messages as they are generated.
* @var Generator<HelgeSverre\ClaudeCode\Types\Messages\Message> $messages
*/
$messages = ClaudeCode::query("Help me refactor this code", $options);
foreach ($messages as $message) {
if ($message instanceof AssistantMessage) {
foreach ($message->content as $block) {
if ($block instanceof TextBlock) {
echo "[CLAUDE] {$block->text}\n";
}
}
} elseif ($message instanceof ResultMessage) {
echo "[DONE] Total Cost: \${$message->totalCostUsd}\n";
}
}
Publish the configuration file:
php artisan vendor:publish --tag=claude-code-configConfigure your settings in config/claude-code.php or use environment variables:
CLAUDE_CODE_SYSTEM_PROMPT="You are a Laravel expert"
CLAUDE_CODE_ALLOWED_TOOLS=Read,Write,Edit
CLAUDE_CODE_PERMISSION_MODE=acceptEdits
CLAUDE_CODE_MODEL=claude-3-sonnetuse HelgeSverre\ClaudeCode\Laravel\Facades\ClaudeCode;
// Simple query
$messages = ClaudeCode::query("Create a new Laravel controller");
// With custom options
$options = ClaudeCode::options()
->systemPrompt("You are a Laravel expert")
->allowedTools(['Read', 'Write', 'Edit'])
->maxTurns(10);
$messages = ClaudeCode::query("Help me build a REST API", $options);use Illuminate\Support\Facades\App;
class MyService
{
private $claude;
public function __construct()
{
$this->claude = App::make('claude-code');
}
public function generateCode(string $prompt): void
{
$messages = $this->claude->query($prompt);
foreach ($messages as $message) {
// Process messages...
}
}
}All configuration options available:
$options = new Options(
// System prompt to set context
systemPrompt: "You are a helpful assistant",
// Additional system prompt to append
appendSystemPrompt: "Always be concise",
// Tools Claude can use
allowedTools: ['Read', 'Write', 'Edit', 'Bash'],
// Tools Claude cannot use
disallowedTools: ['Delete'],
// Permission handling mode
permissionMode: PermissionMode::acceptEdits,
// Custom permission prompt tool
permissionPromptToolName: "MyCustomPrompt",
// Continue existing conversation
continueConversation: true,
// Resume from session ID
resume: "session-abc123",
// Maximum conversation turns
maxTurns: 10,
// Claude model to use
model: "claude-3-sonnet",
// Working directory
cwd: "/path/to/project",
// MCP server configurations
mcpServers: [
'my-server' => new \HelgeSverre\ClaudeCode\Types\ServerConfigs\StdioServerConfig('node', ['server.js']),
],
);The SDK provides strongly-typed message classes with proper DTOs:
System events with typed data for initialization:
use HelgeSverre\ClaudeCode\Types\Messages\SystemMessage;
use HelgeSverre\ClaudeCode\Types\Config\SystemInitData;
if ($message instanceof SystemMessage && $message->subtype === 'init') {
// Strongly typed init data
$initData = $message->data; // SystemInitData instance
echo "Session ID: {$initData->sessionId}\n";
echo "Model: {$initData->model}\n";
echo "Tools: " . implode(', ', $initData->tools) . "\n";
echo "Working Directory: {$initData->cwd}\n";
}Contains content blocks with Claude's responses:
use HelgeSverre\ClaudeCode\Types\Messages\AssistantMessage;
use HelgeSverre\ClaudeCode\Types\ContentBlocks\{TextBlock, ToolUseBlock, ToolResultBlock};
foreach ($message->content as $block) {
match (true) {
$block instanceof TextBlock =>
echo "Text: {$block->text}\n",
$block instanceof ToolUseBlock =>
echo "Tool: {$block->name} with " . json_encode($block->input) . "\n",
$block instanceof ToolResultBlock =>
echo "Result: {$block->content} (Error: " . ($block->isError ? 'Yes' : 'No') . ")\n",
};
}User input and tool feedback:
use HelgeSverre\ClaudeCode\Types\Messages\UserMessage;
use HelgeSverre\ClaudeCode\Types\ContentBlocks\ToolResultBlock;
// Simple text message
$message = new UserMessage("Hello Claude!");
// Tool feedback (from Claude's perspective)
if (is_array($message->content)) {
foreach ($message->content as $block) {
if ($block instanceof ToolResultBlock) {
echo "Tool feedback: {$block->content}\n";
echo "Tool ID: {$block->toolUseId}\n";
echo "Is Error: " . ($block->isError ? 'Yes' : 'No') . "\n";
}
}
}Session completion with usage metrics:
use HelgeSverre\ClaudeCode\Types\Messages\ResultMessage;
echo "Total Cost: \${$message->totalCostUsd}\n";
echo "Duration: {$message->durationMs}ms\n";
echo "API Time: {$message->durationApiMs}ms\n";
echo "Turns: {$message->numTurns}\n";
echo "Session ID: {$message->sessionId}\n";
echo "Input Tokens: {$message->usage['input_tokens']}\n";
echo "Output Tokens: {$message->usage['output_tokens']}\n";Configure Model Context Protocol servers:
use HelgeSverre\ClaudeCode\Types\ServerConfigs\StdioServerConfig;
use HelgeSverre\ClaudeCode\Types\ServerConfigs\SSEServerConfig;
use HelgeSverre\ClaudeCode\Types\ServerConfigs\HTTPServerConfig;
$options = new Options(
mcpServers: [
// Stdio server
'filesystem' => new StdioServerConfig(
command: 'node',
args: ['mcp-server-filesystem.js'],
env: ['NODE_ENV' => 'production']
),
// SSE server
'weather' => new SSEServerConfig(
url: 'https://api.example.com/mcp/sse',
headers: ['Authorization' => 'Bearer token']
),
// HTTP server
'database' => new HTTPServerConfig(
url: 'https://api.example.com/mcp',
headers: ['API-Key' => 'secret']
),
]
);Note: This is NOT related to or the same as Claude Code Hooks
The SDK supports interceptors that allow you to tap into various events during the Claude Code lifecycle. This is useful for logging, monitoring, debugging, or building real-time UI updates.
onQueryStart- Fired when a query beginsonRawMessage- Fired when raw JSON is received from Claude Code CLIonMessageParsed- Fired after a message is parsed into a typed objectonQueryComplete- Fired when the query completes successfullyonError- Fired when an error occurs
use HelgeSverre\ClaudeCode\ClaudeCode;
use HelgeSverre\ClaudeCode\Types\Config\Options;
// Simple logging interceptor
$logger = function(string $event, mixed $data) {
echo "[{$event}] " . json_encode($data) . PHP_EOL;
};
$options = new Options(
interceptors: [$logger]
);
$messages = ClaudeCode::query("Help me code", $options);use HelgeSverre\ClaudeCode\Examples\Interceptors\FileLoggerInterceptor;
$options = new Options(
interceptors: [
new FileLoggerInterceptor('/tmp/claude.log')
]
);use HelgeSverre\ClaudeCode\Examples\Interceptors\MetricsInterceptor;
$options = new Options(
interceptors: [
new MetricsInterceptor() // Tracks token usage, costs, and timing
]
);use HelgeSverre\ClaudeCode\Examples\Interceptors\WebhookInterceptor;
$options = new Options(
interceptors: [
new WebhookInterceptor('https://api.example.com/claude-events')
]
);// Token usage tracker
$tokenTracker = function(string $event, mixed $data) use (&$totalTokens) {
if ($event === 'onMessageParsed' && $data['message'] instanceof ResultMessage) {
$totalTokens += $data['message']->usage['input_tokens'] ?? 0;
$totalTokens += $data['message']->usage['output_tokens'] ?? 0;
}
};
// Real-time progress indicator
$progressTracker = function(string $event, mixed $data) {
switch ($event) {
case 'onQueryStart':
echo "Starting query: {$data['prompt']}\n";
break;
case 'onMessageParsed':
echo "."; // Progress dot for each message
break;
case 'onQueryComplete':
echo "\nQuery completed!\n";
break;
}
};
$options = new Options(
interceptors: [$tokenTracker, $progressTracker]
);// In a service provider
$this->app->bind(Options::class, function ($app) {
$interceptors = [];
// Add logging if enabled
if (config('claude-code.logging.enabled')) {
$interceptors[] = new FileLoggerInterceptor(
storage_path('logs/claude-code.log')
);
}
// Add metrics collection
if (config('claude-code.metrics.enabled')) {
$interceptors[] = new MetricsInterceptor();
}
return new Options(
interceptors: $interceptors,
// ... other options
);
});The SDK provides specific exception types for different failure scenarios:
use HelgeSverre\ClaudeCode\ClaudeCode;
use HelgeSverre\ClaudeCode\Exceptions\CLINotFoundException;
use HelgeSverre\ClaudeCode\Exceptions\CLIConnectionException;
use HelgeSverre\ClaudeCode\Exceptions\ProcessException;
try {
$messages = ClaudeCode::query("Help me code");
} catch (CLINotFoundException $e) {
echo "Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code";
} catch (CLIConnectionException $e) {
echo "Failed to connect to Claude Code: {$e->getMessage()}";
} catch (ProcessException $e) {
echo "Process failed with exit code {$e->exitCode}: {$e->stderr}";
}This SDK is open-source software licensed under the MIT license.