sk-wang / hao-code
PHP agent SDK for Anthropic, OpenAI Responses, and OpenAI Chat Completions APIs.
Requires
- php: ^8.1
- ext-dom: *
- nyholm/psr7: ^1.8
- open-telemetry/exporter-otlp: ^1.0
- open-telemetry/sdk: ^1.0
- open-telemetry/sem-conv: ^1.0
- swaggest/json-schema: ^0.12.43
- symfony/clock: ^6.4 || ^7.0 || ^8.0
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
- symfony/yaml: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^10.5 || ^11.5
Suggests
- ext-pcntl: Required for daemon signal handling (SIGTERM/SIGINT)
- ext-pdo_sqlite: Required for durable run-state recovery and daemon SQLite storage
- ext-posix: Required for daemon PID management and stale-pid detection
- ext-sqlite3: For daemon mode (cron job persistence via SQLite)
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.24.2
- v1.24.1
- v1.24.0
- v1.23.0
- v1.22.0
- v1.21.1
- v1.21.0
- v1.20.2
- v1.20.1
- v1.20.0
- v1.19.10
- v1.19.9
- v1.19.8
- v1.19.7
- v1.19.6
- v1.19.5
- v1.19.4
- v1.19.3
- v1.19.2
- v1.19.1
- v1.19.0
- v1.18.62
- v1.18.61
- v1.18.60
- v1.18.59
- v1.18.58
- v1.18.57
- v1.18.56
- v1.18.55
- v1.18.54
- v1.18.53
- v1.18.52
- v1.18.51
- v1.18.50
- v1.18.49
- v1.18.48
- v1.18.47
- v1.18.46
- v1.18.45
- v1.18.44
- v1.18.43
- v1.18.42
- v1.18.41
- v1.18.40
- v1.18.39
- v1.18.38
- v1.18.37
- v1.18.36
- v1.18.35
- v1.18.34
- v1.18.33
- v1.18.32
- v1.18.31
- v1.18.30
- v1.18.29
- v1.18.28
- v1.18.27
- v1.18.26
- v1.18.25
- v1.18.24
- v1.18.23
- v1.18.22
- v1.18.21
- v1.18.20
- v1.18.19
- v1.18.18
- v1.18.17
- v1.18.16
- v1.18.15
- v1.18.14
- v1.18.13
- v1.18.12
- v1.18.11
- v1.18.10
- v1.18.9
- v1.18.8
- v1.18.7
- v1.18.6
- v1.18.5
- v1.18.4
- v1.18.3
- v1.18.2
- v1.18.1
- v1.18.0
- v1.17.2
- v1.17.1
- v1.17.0
- v1.16.0
- v1.15.0
- v1.14.0
- v1.13.1
- v1.13.0
- v1.12.1
- v1.12.0
- v1.11.1
- v1.11.0
- v1.10.2
- v1.10.1
- v1.10.0
- v1.9.1
- v1.9.0
- v1.8.0
- v1.7.0
- v1.6.1
- v1.6.0
- v1.5.0
- v1.4.0
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.1
- 1.0.0
- v0.4.0
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.5
- v0.2.4
- v0.2.3
- v0.2.2
- v0.2.0
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- v0.0.9
- v0.0.8
- v0.0.7
- v0.0.6
- v0.0.5
- v0.0.4
- v0.0.3
- 0.0.2
- v0.0.1
- dev-agent/hao-code-tool-sandbox/haoc-8
- dev-agent/hao-code-tool-sandbox/haoc-11
- dev-agent/haoc-7-websearch-contract
- dev-codex/sdk-tool-concurrency-v1.21.1
- dev-codex/architecture-closure-v1.21.0
This package is auto-updated.
Last update: 2026-11-02 15:58:52 UTC
README
A framework-free PHP Agent SDK for Anthropic, OpenAI Responses, and OpenAI Chat Completions-compatible APIs.
hao-code lets PHP applications embed an AI coding agent with tools, skills,
streaming output, multi-turn sessions, structured JSON results, durable human
approval, agent teams, cost tracking, abort control, credential pools, and
isolated runtime storage.
For the complete SDK reference, see docs/SDK.md.
Looking for a ready-to-use desktop application powered by HaoCode? See Hao Work.
Requirements
- PHP
^8.1 - Composer; Symfony
^6.4,^7.0, or^8.0components are installed as package dependencies - Optional
pcntlandposixextensions for background agents and agent teams - macOS
/usr/bin/sandbox-execor Linuxbubblewrapwhen using the native local sandbox - Optional
zstdandtarwhen installing the Tokimo guest kernel/rootfs - Optional Linux
bubblewrap, or KVM pluscloud-hypervisorandvirtiofsd, for the Tokimo backend - Optional Windows Virtual Machine Platform plus its one-time SYSTEM service for the Tokimo backend
Install
composer require sk-wang/hao-code
This installs the PHP SDK only. It does not download Tokimo or any native sandbox binaries.
Optional Tokimo sandbox
Install the optional cross-platform sandbox now or at any later time:
# Runner only; use this when you already have a compatible baseRootfs: vendor/bin/hao-code-sandbox install # Recommended first-time setup; also downloads the guest kernel/rootfs: vendor/bin/hao-code-sandbox install --with-runtime # Show the currently installed runner: vendor/bin/hao-code-sandbox status
Composer has no dependency-specific require flag for this, and dependency
package scripts are not executed automatically. For a one-line initial setup,
run:
composer require sk-wang/hao-code && vendor/bin/hao-code-sandbox install --with-runtime
You do not need to install Tokimo separately. The installer downloads the
verified runner and, with --with-runtime, the guest kernel/rootfs artifacts
for the current OS/CPU into the user cache. On macOS and Linux, full runtime
setup may ask for sudo while extracting the rootfs so its Linux uid/gid
ownership is preserved. From a hao-code source checkout, replace
vendor/bin/hao-code-sandbox with bin/hao-code-sandbox.
Quick Start
Set the API key in the process environment:
export ANTHROPIC_API_KEY=your-api-key
Then create example.php:
<?php require __DIR__.'/vendor/autoload.php'; use HaoCode\Sdk\HaoCode; use HaoCode\Sdk\HaoCodeConfig; $result = HaoCode::query('Reply with exactly: HaoCode works'); echo $result->text;
php example.php
The basic query() call does not expose file or shell tools and does not write
a session file. Hao Code reads real process environment variables; it does not load .env files by itself.
Applications that already load .env may pass the key explicitly. Constructing
HaoCodeConfig does not increase authority: its defaults are allowedTools: [],
permissionMode: 'default', and ephemeral: true. Supplying only connection
settings therefore remains text-only and non-persistent:
$config = new HaoCodeConfig( apiKey: getenv('ANTHROPIC_API_KEY') ?: '', );
Agent & Runner
HaoCode also exposes a reusable Agent and Runner API for applications that
want to define an agent once and execute it many times.
use HaoCode\Sdk\Agent; use HaoCode\Sdk\RunOptions; use HaoCode\Sdk\Runner; $agent = new Agent( name: 'reviewer', model: 'claude-sonnet-4', allowedTools: ['Read', 'Grep'], ); $result = Runner::run($agent, 'Review this file', RunOptions::make(cwd: __DIR__));
contextPreset is explicit. Its default, 'coding', preserves the existing
Git, project-instruction, and coding-convention context. Use 'generic' for a
tool-using non-coding agent; tools, skills, memory, output style, permission,
and provider behavior are unchanged:
$agent = new Agent( name: 'support-agent', contextPreset: 'generic', allowedTools: ['LookupOrder'], tools: [new LookupOrderTool()], );
Agents can be composed: one agent can use another as a tool via Agent::asTool().
HaoCode::query() and HaoCode::stream() remain unchanged and are implemented on
top of Runner.
For a less toy-like exercise,
examples/architecture-review-council.php
defines three read-only architecture specialists and a lead reviewer. It runs
against this repository, records every delegated tool lifecycle, rejects empty
specialist reports, and prints aggregate usage plus a council verdict. Provider
settings come from the normal SDK Runtime; ARCH_COUNCIL_MODEL optionally
overrides the model, ARCH_COUNCIL_BASE_URL selects an Anthropic-compatible
endpoint, and ARCH_COUNCIL_BUDGET_USD enables a shared budget only when
trusted pricing exists for that model.
examples/agent-shapes-lab.php exercises five
separate agent contracts against one provider: a custom-tool agent, structured
output, streaming, multi-turn conversation, and Agent-as-Tool delegation. Run
the full matrix or select one with --shape=...; each shape has its own output
acceptance gate, so an empty child response cannot be hidden by a successful
parent report.
What It Provides
| Area | Capability |
|---|---|
| Agent execution | One-shot queries, streaming responses, multi-turn conversations, session resume, durable human approval |
| Providers | Anthropic, OpenAI Responses API, OpenAI Chat Completions-compatible gateways |
| Tools | Built-in file, search, patch, shell, web, MCP, task, team, memory, and planning tools, plus custom PHP tools |
| Sandbox | Local workspace, OS-native isolation, optional Tokimo runners, or Alibaba Cloud AgentRun for sandbox-scoped file and shell tools |
| Skills | Prompt-packaged domain guidance through SdkSkill |
| Structured output | JSON schema guided responses via HaoCode::structured() |
| Multimodal input | Image input via file path, URL, data URI, or pre-built blocks in query(), stream(), and Conversation::send() |
| Runtime control | Working directory, allowed tools, denied tools, permission mode, max turns, max tokens, thinking options |
| Operations | Cost budget, usage metadata, abort controller, callbacks for text/tool/turn events |
| State | Session IDs, conversation handles, memory summary levels, custom memory storage path |
| Reliability | Credential-pool failover on rate limits, provider abstraction, SDK-only runtime without framework dependencies |
The built-in Read tool returns text only. It does not return images or PDFs
without extractable text as base64 tool results; pass images through the SDK
image-input APIs instead. File mutation is also fail closed: a failed or partial
Read does not authorize Write or Edit, and an external revision change
requires another complete Read before mutation.
Main APIs
| Need | API |
|---|---|
| One-shot query | HaoCode::query() |
| Streaming messages | HaoCode::stream() |
| Multi-turn conversation | HaoCode::conversation() |
| Resume a session | HaoCode::resume() |
| Continue latest session | HaoCode::continueLatest() |
| Resume human approval | HaoCode::resumeInterrupt() |
| Stream approval resume | HaoCode::streamResumeInterrupt() |
| Structured JSON result | HaoCode::structured() |
Configuration
Pass HaoCodeConfig when you need explicit runtime configuration:
use HaoCode\Sdk\HaoCodeConfig; $config = new HaoCodeConfig( apiKey: getenv('OPENAI_API_KEY') ?: '', providerType: 'openai_chat', baseUrl: 'https://api.openai.com', model: 'gpt-4.1', maxTokens: 4096, cwd: __DIR__, maxTurns: 30, permissionMode: 'default', allowedTools: ['Read', 'Grep', 'Glob'], disallowedTools: ['Bash'], );
If no explicit config is provided, the SDK reads environment and settings
values such as ANTHROPIC_API_KEY, HAOCODE_MODEL, HAOCODE_API_BASE_URL, and
HAOCODE_MAX_TOKENS. When the selected provider type is openai or
openai_chat, OPENAI_API_KEY is used if no explicit apiKey or matching
provider-entry key is set; it is not an Anthropic fallback.
For openai and openai_chat, a model must also be supplied explicitly or by
the selected provider entry; the Anthropic default is never sent to those
providers.
providerType accepts only anthropic, openai, and openai_chat (plus a
few documented aliases such as openai_responses / chat_completions). Any
other non-empty value throws at HaoCodeConfig construction — unknown types
never silently fall back to Anthropic, so an OpenAI key cannot be sent to an
Anthropic endpoint by typo.
Before it constructs a provider, sandbox, or MCP connection, the SDK preflights
the requested Agent, provider, model, endpoint, tools, sandbox, and permission
mode. After AgentLoopFactory assembles the run, the same run guard binds to the
actual ToolRegistry; implicit tools such as AskUserQuestion therefore cannot
fall outside the manifest. The guard runs again for every runtime setting
mutation and immediately before every provider request. Unsupported changes are
rolled back atomically. Unknown capabilities on new models or custom gateways
remain allowed for compatibility. oauthBearer: true is supported by the
Anthropic wire adapter only; selecting either OpenAI wire format with that
option fails before provider I/O.
The runtime Config tool treats active_provider as one connection identity:
provider type, key, model, endpoint, and token limits come from the newly
selected provider rather than leaking across from the old one. Run-level OAuth
mode and custom headers remain explicit; an incompatible switch is rejected and
rolled back instead of silently weakening authentication. Credential pools and
cost tracking also resolve the current provider on each request. Runtime
api_base_url changes may change only the path on the current HTTP(S) origin;
configure and select a named provider to change hosts.
For deepseek-v4-flash, enabling thinking sends DeepSeek's explicit thinking
contract. A thinking budget of 32000 or more selects maximum reasoning effort,
and HaoCode preserves reasoning_content across multi-turn tool calls.
For Anthropic models that use adaptive thinking (including Opus 4.8 and current
Claude 5 IDs), thinkingBudget maps to effort tiers (low / medium /
high / max) unless settings effort_level is set explicitly. Manual
extended-thinking models still use thinkingBudget as a token budget.
Tools, permission bypass, and durable storage are independent opt-ins. An
unattended full agent must state all three explicitly, for example
allowedTools: ['*'], permissionMode: 'bypass_permissions', and
ephemeral: false. Do this only inside a trust boundary appropriate for the
tools being exposed.
For human-in-the-loop runs, hitlMode selects the approval mode. The default
is 'smart' (resolved from haocode.hitl_mode / HAOCODE_HITL_MODE when the
constructor value is null): routine actions are fast-pathed by rules,
gray-area actions are reviewed by a model (hitlReviewModel, defaulting to the
run's model), and only dangerous actions interrupt for a human decision.
'ask' interrupts every configured action, and 'auto' suppresses tool
interrupts entirely. Every automatic decision surfaces on the stream as an
auto_decision message. In 'smart' mode, two fast paths settle Bash
actions without the guardian model: sandbox containment (a gray-area command
that will run inside an isolating sandbox is approved directly) and a
user-saved allow list pointed to by hitlAllowlistPath (exact- and prefix-match
v1+v2 rules; every segment of a compound command must match, so
git commit && rm -rf / never slips through). See
Smart HITL modes in the SDK reference.
v1.8.0 behavior change:
v1.7.0constructedHaoCodeConfigwith all tools, permission bypass, and durable storage by default. Existing trusted callers that need the old behavior must now opt in to those three settings explicitly.
Sandbox Runtime
Use a sandbox when the agent needs file or shell tools but must not mutate the
PHP host project directory. Sandbox mode replaces Read, Write, Glob, and
Grep with sandbox-scoped tools. Set mode: 'full' to also replace Bash with
a sandbox-scoped shell. Sandbox configuration disables Edit, apply_patch,
NotebookEdit, LSP, worktree tools, Agent, and SendMessage. Other host-only
tools, including task/team tools, are not sandbox replacements; use an
explicit allowedTools list as shown below and omit them unless needed. Legacy
cron tool classes are not registered by the default runtime because no prompt
execution driver is wired.
The replacement boundary is fixed by mode:
| Tool family | filesystem |
full |
Boundary |
|---|---|---|---|
Read, Write, Glob, Grep |
Sandbox | Sandbox | All file reads/searches use the sandbox backend |
Bash |
Disabled | Sandbox | Only full runs shell commands inside the sandbox |
Edit, apply_patch, NotebookEdit, LSP, worktree, Agent, SendMessage |
Disabled | Disabled | Not sandbox replacements and unavailable while sandboxed |
| task/team, custom, and MCP tools | Host-side only | Host-side only | Never implicitly moved into the sandbox; allow explicitly only when the host wants them |
An explicit request for a disabled sandbox tool fails before the sandbox is
created. allowedTools: ['*'] means all tools available in the selected
sandbox mode; it does not require unavailable host-only tools or Bash in
filesystem mode.
Overwriting an existing sandbox file also requires a complete current Read.
Local, native, and Tokimo backends compare the receipt while publishing the
replacement atomically. AgentRun rechecks the remote bytes immediately before
writing, but its file API has no conditional-write primitive, so that remote
check cannot eliminate a concurrent change between the check and write.
The read-before-write receipt is committed only when the current model tool
batch becomes visible to the model. A Read and a Write/Edit in the same
tool batch cannot authorize one another; issue the mutation in the next batch.
Host and sandbox text reads share the same bounds: offset up to 1,000,000, limit up to 10,000 lines, and at most 1,000,000 bytes for one line or the rendered result.
AgentRun Write results expose metadata['writeSafety'] === 'recheck_only'
and metadata['conditionalWrite'] === false. Local, Native, and Tokimo writes
use the stronger conditional revision-aware path.
Choose the backend by the isolation boundary you need:
| Backend | Hosts | Use it for |
|---|---|---|
local() |
Any supported PHP host | File workspace isolation without untrusted Bash; shell commands still run as normal host processes |
native() |
macOS and Linux | Lightweight local process isolation through Seatbelt or bubblewrap |
tokimo() |
macOS arm64, Linux amd64/arm64, Windows amd64 | Recommended cross-platform boundary for agent-generated or untrusted shell commands; installed separately on demand |
agentRun() |
Any host with AgentRun access | Remote cloud isolation when commands and files must stay off the PHP host |
Sandbox Bash captures at most 100,000 bytes. Native, Tokimo, and AgentRun
apply that bound independently to stdout and stderr; Local uses one shared budget
across both streams. outputLimited uses exit code 1, timeout uses 124, and
abort uses 130; normal sandbox Bash execution metadata includes all three
flags.
AgentRun command HTTP bodies and Tokimo runner JSONL responses also have a
2 MiB transport limit before JSON or base64 decoding. This protects PHP memory;
it does not raise the 100,000-byte command-output limit. An oversized transport
response returns the same outputLimited: true, exit-code 1 result. Tokimo
also discards the oversized runner process, then starts a clean one for the next
command while keeping the sandbox workspace.
For a portable full sandbox, prefer tokimo(). It is intentionally not part of
the default Composer install; run
vendor/bin/hao-code-sandbox install --with-runtime before first use or at any
later time. Use local() when only sandbox-scoped file tools are required.
use HaoCode\Sdk\HaoCode; use HaoCode\Sdk\HaoCodeConfig; use HaoCode\Sdk\Sandbox\SandboxConfig; $result = HaoCode::query('Review this project and write notes to notes.md', new HaoCodeConfig( cwd: __DIR__, sandbox: SandboxConfig::local( mode: 'filesystem', // Read/Write/Glob/Grep only; Bash disabled sync: 'upload-cwd', // copy cwd snapshot into /workspace ), allowedTools: ['Read', 'Write', 'Grep', 'Glob'], permissionMode: 'bypass_permissions', // isolated workspace, unattended run ));
local() isolates the workspace path but executes mode: 'full' commands as a
normal host process. For agent-generated commands, use the native backend:
$config = new HaoCodeConfig( cwd: __DIR__, sandbox: SandboxConfig::native( sync: 'upload-cwd', network: 'blocked', // opt in with "allow-all" when the task needs it ), allowedTools: ['Read', 'Write', 'Grep', 'Glob', 'Bash'], permissionMode: 'bypass_permissions', // native sandbox contains mutations );
The native backend uses macOS Seatbelt or Linux bubblewrap, exposes only the
sandbox workspace for writes, removes inherited secrets from the command
environment, and blocks networking by default. It fails closed when the native
engine is unavailable. Linux hosts must install bubblewrap; macOS uses the
system /usr/bin/sandbox-exec executable.
Tokimo cross-platform sandbox
SandboxConfig::tokimo() uses an optional native runner for macOS arm64, Linux
amd64/arm64, or Windows amd64. Neither the runner nor the larger VM images are
part of the Composer package. Install the runner only, or install the complete
runtime in one step:
vendor/bin/hao-code-sandbox install
vendor/bin/hao-code-sandbox install --with-runtime
# Check whether a runner is available:
vendor/bin/hao-code-sandbox status
Both install commands are safe to run later, after the original
composer require. Files are SHA-256 verified and stored in the user's cache.
Hao Code reports the same install command if tokimo() is used before the
runner is installed. From a source checkout, use bin/hao-code-sandbox.
The --with-runtime command prints the resulting baseRootfs directory. Pass
that path to the SDK:
$baseRootfs = getenv('HAOCODE_SANDBOX_ROOTFS'); if (! is_string($baseRootfs) || $baseRootfs === '') { throw new RuntimeException('Set HAOCODE_SANDBOX_ROOTFS to the setup output.'); } $config = new HaoCodeConfig( cwd: __DIR__, sandbox: SandboxConfig::tokimo( baseRootfs: $baseRootfs, network: 'blocked', ), allowedTools: ['Read', 'Write', 'Grep', 'Glob', 'Bash'], permissionMode: 'bypass_permissions', // the sandbox contains mutations );
Hao Code keeps one sandbox session alive for the SDK run, so the VM and its
filesystem are shared across commands. On Linux, Tokimo uses its micro-VM backend
when KVM and its VM helpers are available and otherwise falls back to bubblewrap.
Install cloud-hypervisor and virtiofsd before running the setup command to
link the micro-VM helpers into the cache; install bubblewrap for the fallback.
On Windows, install the downloaded haocode-sandbox-svc-windows-amd64.exe once
with administrator rights before using the Hyper-V backend.
Alibaba Cloud AgentRun
SandboxConfig::agentRun() uses Alibaba Cloud AgentRun as a remote temporary
filesystem and execution environment. Use it when the PHP server should not touch
local files or run untrusted commands locally.
export AGENTRUN_ACCOUNT_ID=your-account-id export AGENTRUN_API_KEY=your-api-key export AGENTRUN_TEMPLATE_NAME=your-template-name export AGENTRUN_REGION=cn-hangzhou php scripts/agentrun-verify.php
Use AGENTRUN_TEMPLATE_NAME to create a fresh temporary sandbox from a template.
Only set AGENTRUN_SANDBOX_ID when you already have a live sandbox instance ID;
a template ID is not a sandbox instance ID.
$config = new HaoCodeConfig( sandbox: SandboxConfig::agentRun( accountId: getenv('AGENTRUN_ACCOUNT_ID') ?: throw new RuntimeException('Set AGENTRUN_ACCOUNT_ID.'), templateName: getenv('AGENTRUN_TEMPLATE_NAME') ?: throw new RuntimeException('Set AGENTRUN_TEMPLATE_NAME.'), apiKey: getenv('AGENTRUN_API_KEY') ?: null, mode: 'full', remoteCwd: '/tmp', ), allowedTools: ['Read', 'Write', 'Bash'], permissionMode: 'bypass_permissions', // remote sandbox contains mutations );
For the current AgentRun code-interpreter template, write demo files under
/tmp/workspace; creating /workspace at the filesystem root can be denied by
the container. See examples/agentrun-ml-clustering-agent.php for a complete
agent-generated data + Python k-means demo.
AgentRun Glob and Grep prune default heavy directories, bound visited files
and returned results, and apply glob filters to relative paths. A bounded
remote search keeps the normal text result shape and adds metadata such as
searchLimited, resultLimited, visitedLimited, and residualDifferences.
Sandbox Glob keeps the host heading and emits an explicit bounded-result
notice; sandbox paths stay in the remote namespace so they can be passed to
sandbox Read. Search failures use Glob search failed: and Grep search failed: prefixes.
Streaming
Use HaoCode::stream() when the caller needs incremental output:
foreach (HaoCode::stream('Explain PHP Fibers in three sentences') as $message) { if ($message->isError()) { throw new RuntimeException($message->error); } if ($message->text !== null) { echo $message->text; } }
Terminal results expose RunTerminationReason through
QueryResult::$terminationReason and terminal
Message::$terminationReason. Values are normal, cancelled,
budget_exhausted, turn_limit, and repeated_tool_failure. Use this enum for
control flow; the v1 compatibility strings such as (aborted) remain unchanged
and are not status indicators.
Provider-controlled streaming input is bounded: provider SSE lines and
multiline events over 4 MiB fail with a protocol_error, and provider error
bodies are retained only as bounded prefixes. MCP stream/frame parsing applies
the same fail-closed principle. The normalized text and thinking accumulators
also reject more than 1,000,000 bytes per stream instead of retaining an
unbounded response in memory.
One-shot streams own their runtime resources. Once Runner::stream(),
HaoCode::stream(), or HaoCode::streamResumeInterrupt() yields a terminal
result or error, its sandbox, MCP connections, and abort subscription have
already been released; retaining that completed Generator does not keep the run
alive. A further durable interrupt still preserves its sandbox lease for the
next resume.
Conversations
Use a conversation handle when later prompts should keep the same message history and session:
$conversation = HaoCode::conversation(new HaoCodeConfig( cwd: __DIR__, allowedTools: ['Read', 'Glob', 'Grep'], ephemeral: false, )); $conversation->send('Read the service layer and remember the architecture.'); $result = $conversation->send('Now review the newest changes.'); echo $result->text; echo $conversation->getSessionId();
HaoCode keeps the system prompt byte-stable for the lifetime of a conversation
and grows message history append-only. Volatile Git status is attached to the
initial user turn instead of rewriting the system prefix, improving automatic
prefix-cache reuse on DeepSeek and other compatible providers. DeepSeek cache
hits are reported through usage['cache_read_tokens']. usage['input_tokens']
is cumulative for the run/conversation; use usage['last_turn_input_tokens']
for the current context-window occupancy.
HaoCode::resume($sessionId) restores tools against the session transcript's
canonical working directory, not the current PHP process cwd. If you pass a
config cwd that differs from that directory, resume fails unless you set
allowCwdOverride: true. HaoCode::continueLatest($cwd) injects the lookup
$cwd into the resume config automatically.
Durable sessions also append versioned, provider-neutral run events and
incremental checkpoints to the existing session JSONL. Legacy session records
remain readable and are still the transcript authority. For hosts that need
transactional tool recovery across workers, set HAOCODE_RUN_STORE=sqlite and
optionally HAOCODE_RUN_DATABASE_PATH=/protected/path/run-state.sqlite (PDO
SQLite is required). SQLite mode adds idempotency claims, leases and fencing;
an uncertain mutating tool becomes unknown and is never retried automatically.
It deliberately executes tools sequentially so a forked process cannot reuse a
database connection across the transaction boundary. This is at-least-once
recovery with fail-closed mutations, not an exactly-once guarantee.
The recovery protocol is tested across independent PHP processes and real
SIGKILL boundaries; a Worker daemon/Queue adapter remains host-owned. Internal
event export is always redacted, but Replay reads raw facts, so protect and
retain the run-state JSONL/SQLite files as sensitive application data.
Human approval
Human-in-the-loop runs are durable and non-blocking: the SDK pauses, returns a
serializable interrupt, and the host resumes it in a later HTTP request or worker
job. Pass the original HaoCodeConfig back to resumeInterrupt() so the SDK can
restore the same tool and sandbox boundary.
If a later process first creates a handle with HaoCode::resume($sessionId, $config),
then calls $conversation->resumeInterrupt(...), the checkpoint sandbox is
reattached to that rebuilt handle too. A follow-up send() or
streamResumeInterrupt() therefore sees files created before the interrupt.
If that streaming resume pauses again, only that checkpoint lease is retained;
an unrelated fresh sandbox created for the loaded handle still follows its
configured cleanup policy when the handle closes.
use HaoCode\Sdk\HaoCode; use HaoCode\Sdk\HaoCodeConfig; use HaoCode\Sdk\HumanDecision; use HaoCode\Sdk\HumanInterruptException; $config = new HaoCodeConfig( cwd: __DIR__, allowedTools: ['Read', 'Write', 'Bash'], ephemeral: false, interruptOn: ['Bash' => true, 'Write' => true], enableAskUser: true, ); try { $result = HaoCode::query('Create report.txt', $config); } catch (HumanInterruptException $e) { // Persist or return $e->interrupt->toArray() to your UI. $result = HaoCode::resumeInterrupt( $e->interrupt->sessionId, $e->interrupt->id, [HumanDecision::approve($e->interrupt->actions[0]->id)], $config, ); }
edit changes only tool arguments, reject returns error feedback to the model,
and respond supplies a successful tool result. Hard permission denials cannot
be overridden. HITL cannot be combined with ephemeral: true.
Streaming hosts resume the same checkpoint with HaoCode::streamResumeInterrupt();
it yields normal stream messages and exactly one final result, unless another
interrupt pauses the run again.
The facade closes its temporary restored Conversation before yielding a terminal
result or error; if it yields another interrupt, it detaches rather than
deletes the checkpoint's sandbox lease.
Resume preserves the effective inline Skill tool scope and cumulative
token/cost totals. A synchronous worktree Agent is finalized after its resumed
run; retained changes are reported in the final text and usage metadata.
The checkpoint also carries the effective cwd, project/worktree identity, model
and prompt settings, allowed tools and Skill scope, remaining turns, cumulative
usage/cost, budget-ledger identifiers, and sandbox lease identity. Credentials
are excluded; the current resume config still revalidates the security policy.
Once claimed, an interrupt moves to resolving and is never auto-retried (tool
side effects may already have run). If resume fails after the claim — provider
timeout, tool error, session write failure, or post-claim setup errors — the
session records a terminal interrupt_failed state with the error and a
side-effect hint instead of staying stuck in resolving. A long-lived
Conversation that resumes an interrupt rebuilds its loop and restores the
session / live-run working directory for later send() calls. Branching a
session that still has a pending/resolving interrupt is refused.
Structured Output
Use structured() for machine-readable results:
$result = HaoCode::structured('Classify: "payment failed"', [ 'type' => 'object', 'properties' => [ 'category' => ['type' => 'string'], 'priority' => ['type' => 'string', 'enum' => ['low', 'medium', 'high']], ], 'required' => ['category', 'priority'], ]); echo $result->category;
Schema and JSON-parse retries always reuse one in-memory Conversation (even
when ephemeral: true) so tool side effects and transcript history stay in the
same agent run. ephemeral only controls durable session persistence, not retry
isolation. Correction turns ask for fixed JSON only and tell the model not to
repeat completed side effects. StructuredResult::toJson() preserves Unicode
and slashes and throws JsonException when the result cannot be JSON-encoded;
__toString() uses the same encoding policy with pretty printing.
MCP Tools
Define MCP servers in <cwd>/.haocode/settings.json (or globally in
~/.haocode/settings.json). For example, Context7 uses Streamable HTTP:
{
"mcp_servers": {
"context7": {
"transport": "http",
"url": "https://mcp.context7.com/mcp",
"enabled": true
}
}
}
Enable the discovered, normalized tool names explicitly for the SDK run:
$result = HaoCode::query('Use Context7 to find the current Symfony Process API', new HaoCodeConfig( cwd: __DIR__, allowedTools: [ 'mcp__context7__resolve_library_id', 'mcp__context7__query_docs', ], ));
Hao Code connects only when the run allows MCP tools, registers them for that
run, and disconnects when the run closes. Stdio servers inherit only basic
process-launch variables such as PATH and HOME; pass required credentials
through that server's explicit env map instead of relying on host inheritance.
When a sandbox is active, allowedTools: ['*'] does not enable host-side MCP
servers; list the required mcp__... tools explicitly.
For direct MCP host integrations, call McpConnectionManager::poll() during
idle time (or make the next MCP request) so stdio notifications and reverse
requests are serviced; stdio has no background reader thread. The SDK run wires
this cooperative event pump automatically. ensureConnected($name) is the
reconnecting manager entry point. McpClient::setRoots() only updates the
host-side roots/list response and does not automatically notify an already
connected server, so set roots before connecting when initialization needs them.
The http transport implements MCP Streamable HTTP 2025-11-25: JSON and
incremental SSE responses, server-initiated requests and notifications, the
optional GET event stream, Last-Event-ID resumption, session re-initialization
after HTTP 404, and best-effort DELETE on close. Static Bearer headers remain
supported. For headless OAuth client-credentials or refresh-token flows, keep
secrets in environment variables and reference only their names:
{
"mcp_servers": {
"private-api": {
"transport": "http",
"url": "https://mcp.example.com/mcp",
"oauth": {
"token_endpoint": "https://auth.example.com/oauth/token",
"client_id": "hao-code",
"client_secret_env": "PRIVATE_MCP_CLIENT_SECRET",
"refresh_token_env": "PRIVATE_MCP_REFRESH_TOKEN",
"scope": "mcp:tools"
}
}
}
}
access_token_env can provide an existing access token. On HTTP 401 Hao Code
refreshes it once when the OAuth configuration contains enough credentials.
Interactive browser authorization and dynamic client registration are not
performed by the framework-free SDK; obtain those credentials in the host
application and expose them through the configured environment variables.
Custom Tools
Define domain-specific PHP tools by extending SdkTool:
use HaoCode\Sdk\HaoCode; use HaoCode\Sdk\HaoCodeConfig; use HaoCode\Sdk\SdkTool; $lookupOrder = new class extends SdkTool { public function name(): string { return 'LookupOrder'; } public function description(): string { return 'Look up an order by ID.'; } public function parameters(): array { return [ 'order_id' => ['type' => 'string', 'required' => true], ]; } public function handle(array $input): string { return json_encode(['status' => 'paid']); } // Pure lookups opt into Plan-mode approval. Forking remains disabled. public function isReadOnly(array $input): bool { return true; } }; $result = HaoCode::query('Check order A123', new HaoCodeConfig( allowedTools: ['LookupOrder'], tools: [$lookupOrder], ));
Custom tools and sandbox replacement tools are exposed only when their exact
names appear in allowedTools (or when allowedTools: ['*'] is used);
disallowedTools always wins. By default SdkTool is neither read-only nor
concurrency-safe. Override isReadOnly() only for pure queries; this allows
Plan-mode approval but still executes in the caller process. Forking requires a
second explicit opt-in via isConcurrencySafe(), and is appropriate only when
the tool's complete dependency graph is safe after pcntl_fork. Framework
containers, database connections, and cURL clients generally are not. Any
stateful or non-concurrency-safe tool remains an execution barrier for later
calls in the same model response.
Each public run gets its own agent loop, message history, cost tracker, and
tool registry. Custom SdkTool objects do not have a cloneability contract,
however: cloneable instances are copied for isolated child registries, while
non-cloneable instances may remain shared. Keep custom tools stateless, create
one instance per concurrent run, or synchronize their mutable state when using
Swoole, RoadRunner, or another long-lived worker. The SDK runtime and its
in-memory rate-limit bookkeeping are process-local; use process isolation or an
application-owned shared store when worker-wide coordination is required.
maxTurns must be at least 1. In addition to that hard turn budget, the loop
stops repeating a valid tool call after three consecutive identical tool-error
batches and makes one final no-tool request. Provider retries use bounded jitter
when no Retry-After header is supplied, while an explicit server delay is
honored as received.
When a ToolInputSchema uses only JSON Schema, a malformed self-contained
schema remains an intentional allow-through compatibility fallback so one bad
MCP schema does not disable every call to that tool. External $ref targets
are rejected without I/O. Structured response schemas use a stricter path and
are rejected before the model call when unusable.
Custom Skills
Use SdkSkill to package reusable prompt guidance:
use HaoCode\Sdk\SdkSkill; $skill = new SdkSkill( name: 'security-review', description: 'Review code for common security risks.', prompt: 'Check $ARGUMENTS for injection, auth bypass, secrets, and unsafe IO.', // Full tool names, or Claude-style patterns such as Bash(cargo:*). // Patterns are enforced at call time — they are never stripped to a wider grant. allowedTools: ['Read', 'Grep', 'Bash(cargo:*)'], model: 'haiku', // Anthropic tier alias (or a full model id) context: 'inline', // use 'fork' for an isolated child agent ); $result = HaoCode::query('Use security-review on app/Auth.php', new HaoCodeConfig( allowedTools: ['Skill', 'Read', 'Grep', 'Bash'], skills: [$skill], ));
To use an existing Claude Skill catalog without copying it, opt in explicitly:
$result = HaoCode::query('Use the matching skill for this task', new HaoCodeConfig( allowedTools: ['Skill', 'Read', 'Grep'], skillDirectories: [getenv('HOME').'/.claude/skills'], recursiveSkillDiscovery: true, ));
Skill-specific tool restrictions and model overrides are enforced during the
active skill scope (inline for the rest of the turn; fork only inside the child
agent). Claude-style entries such as Bash(cargo:*) keep the command pattern —
Bash(cargo:*) does not silently become unrestricted Bash, and shell
chaining / expansion (cargo test; rm -rf /, pipes, $(), redirections) is
rejected. Nested inline skills intersect their capability lists. Model
overrides use the same provider-aware alias rules as Agent tools: haiku /
sonnet / opus expand on Anthropic; full model IDs pass through; Anthropic
aliases on non-Anthropic providers fail closed. Standalone skill shell
directives are forwarded to the normal Bash tool, so permission checks,
hooks, and skill scope still apply. Additional directories are never loaded
implicitly.
Credentials And Budgets
Use credential pools when you have multiple API keys, and cost budgets when the caller needs a shared post-response spending guard:
use HaoCode\Sdk\Credential; use HaoCode\Sdk\CredentialPool; $pool = new CredentialPool; $pool->addMany('anthropic', [ new Credential(apiKey: getenv('ANTHROPIC_API_KEY_1') ?: ''), new Credential(apiKey: getenv('ANTHROPIC_API_KEY_2') ?: ''), ]); $config = new HaoCodeConfig( credentialPool: $pool, maxBudgetUsd: 1.00, );
exhaustedTtlSeconds and an explicit retryAfterSeconds must be finite,
non-negative numbers. Each exhausted credential keeps the TTL used for that
exhaustion, so a provider-specific retry delay does not silently replace the
pool default for other credentials.
Credential-pool exhaustion and RateLimitTracker state are in-memory and
process-local. They coordinate calls handled by one PHP process only; a
multi-worker deployment needs an application-owned shared implementation if
workers must observe each other's rate limits. When a provider supplies no
Retry-After, SDK exponential retries add bounded jitter to avoid synchronized
retries across clients. Pool buckets are selected from the provider type at the
start of every request, including after a runtime active_provider change; a
credential from the previous provider is never reused for the new wire adapter.
Built-in USD budgets use exact pricing for the Claude models listed by this
release. Unknown or non-Anthropic models report cost_available: false;
requesting maxBudgetUsd for one of those models fails before a request is sent
instead of applying an unrelated fallback price. If a priced request returns an
unknown deployment alias, usage keeps the already validated request-model price
rather than becoming unpriced after the spend. The budget is one shared,
process-safe total across the root run, child/Team/background agents, forked
skills, structured retries, and durable HITL resumes. Enforcement is a
shared post-response spending guard (checked before the next request and
after each usage record), not a pre-reserved hard cap: a single in-flight call
can finish slightly over the limit, and parallel child agents may overshoot
further before their costs are merged. On HITL resume, the effective limit is
min(snapshot limit, current config maxBudgetUsd) so a tighter caller budget
is never ignored.
Callbacks And Abort
HaoCodeConfig supports callbacks for text and thinking deltas, tool starts,
tool completions, and turn starts. Streaming APIs also yield a turn message at
the start of every agent turn. It supports AbortController for external cancellation:
use HaoCode\Sdk\AbortController; $abort = new AbortController(); $config = new HaoCodeConfig( abortController: $abort, onText: fn (string $delta) => print $delta, onThinking: fn (string $delta) => error_log("thinking: {$delta}"), onToolStart: fn (string $name, array $input) => error_log("tool: {$name}"), );
Cancellation is checked again after each provider request. If it wins the race
with an otherwise completed response, including the no-tool final answer after
the tool-turn limit, the operation returns (aborted) without persisting that
assistant response.
Storage And Memory
Runtime data is stored under ~/.haocode/storage by default when installed
through Composer. Set HAOCODE_STORAGE_PATH for an application-specific
runtime directory. Background Agent, Team, and Task state is isolated under
app/haocode/background-agents, app/haocode/teams, and
app/haocode/tasks inside that runtime directory.
Background admission and mailboxes are bounded per owner run. Defaults are 8
active agents, 128 queued messages, 65,536 bytes per message, and 1 MiB per
mailbox. Override them with
HAOCODE_BACKGROUND_AGENT_MAX_ACTIVE_PER_RUN,
HAOCODE_BACKGROUND_AGENT_MAILBOX_MAX_MESSAGES,
HAOCODE_BACKGROUND_AGENT_MESSAGE_MAX_BYTES, and
HAOCODE_BACKGROUND_AGENT_MAILBOX_MAX_BYTES. Every value must be positive;
invalid configuration fails during runtime startup.
Long-term memory uses compact l0 summaries in the system prompt by default;
l1 provides a larger overview and l2 injects original content. Supplying
memoryStoragePath explicitly also enables memory injection for text-only runs:
$memoryPath = __DIR__.'/var/haocode-memory.json'; $config = new HaoCodeConfig( memorySummaryLevel: 'l1', memoryStoragePath: $memoryPath, );
Use the public store API to seed or manage memory, and pass the same store to a run when the agent needs on-demand detail:
use HaoCode\Sdk\Memory\JsonMemoryStore; $memory = new JsonMemoryStore($memoryPath); $memory->write('response_style', 'The user prefers concise answers.', 'preference'); $config = new HaoCodeConfig( memoryStore: $memory, // takes precedence over memoryStoragePath allowedTools: ['MemoryRead'], );
MemoryWrite and MemoryDelete are never exposed unless explicitly listed in
allowedTools. They are state-changing tools, so normal permission checks also
apply; their tool policy limits use to explicit remember/update/forget requests
and forbids storing secrets. JsonMemoryStore uses a file lock and atomic file
replacement. Implement MemoryStoreInterface to use a database or another
application-owned store.
Examples
| Example | Purpose |
|---|---|
examples/code-review-agent.php |
Code review workflow |
examples/agentrun-ml-clustering-agent.php |
AgentRun sandbox ML clustering demo |
examples/support-ops-agent.php |
End-to-end support operations agent |
examples/weather-agent.php |
Custom tool example |
examples/sdk-suite/ |
Focused examples for query, streaming, conversation, structured output, abort, credential pools, patching, MCP, and provider matrix |
Built-in Web Search
WebSearch concurrently queries Bing, DuckDuckGo, Sogou, 360 Search, and
Yahoo by default, then deterministically deduplicates and ranks up to eight
results. Calls may select a non-empty subset with the optional engines tool
argument; supported IDs are bing, duckduckgo, sogou, 360, and yahoo.
Google and other engines are not implicitly enabled.
The model still receives the existing Markdown result list. Hosts can use
onToolComplete and inspect ToolResult::$data for schema-v1 result cards,
engine provenance, scores, per-engine status, and partial-failure state without
parsing that Markdown. See Complete SDK reference
for the input and data contracts.
Documentation
Source Quality Gates
Every Git-tracked PHP file is limited to 800 physical lines. The limit is a guardrail for a cohesive domain file, not a target to split by. The rule has no legacy allowlist and covers production code, tests, examples, and repository scripts. Run the standalone check with:
composer run quality:file-size
composer test runs this check before test-discovery validation and the full
non-live PHPUnit suite. GitHub Actions enforces the same contract on Linux and
Windows. Large implementations keep their existing public class as the API and
extract a domain collaborator or a domain-named aspect; a *Concern suffix is
not a substitute for a responsibility boundary.
Version
Published versions are identified by Git tags and Packagist. This source line
is based on v1.23.0. Notable changes since v1.10.0:
-
v1.23.0— Background agents andrun_in_backgroundBash commands now report themselves when they finish, instead of waiting to be polled, and background Bash output is finally reachable through aBashOutputtool. Plan mode gains a plan file, a system-prompt section, and anExitPlanModethat actually resolves the phase: human approval on a durable session, otherwise the plan is handed back with the newRunTerminationReason::PlanReady. New opt-ingoal,goalVerificationRounds,goalReminder, andplanExitPolicysettings keep long runs on task.WebSearchasks for a date on time-sensitive queries. All public API changes are additive; hosts with an exhaustivematchoverRunTerminationReasonmust add aPlanReadyarm. -
v1.22.0— WebFetch gains opt-in main-content extraction (extract, withkeywordsto weight the blocks that matter), flags bot-challenge pages and client-rendered shells instead of returning them as content, reports the body of a 4xx/5xx response alongside its status code, and presents a browserUser-Agentso CDN user-agent blacklists no longer read as dead links. Default WebFetch output is unchanged. Adds anext-domrequirement. -
v1.21.0— Adds typed run termination reasons, fail-closed JSON Schema tool registration, one Fiber-backed streaming lifecycle, bounded background-agent admission/mailboxes, and immutable run bootstrap/config adaptation. Existing v1 response text and provider wire formats remain compatible. -
v1.20.2— Keeps RunEvent Store contract tests compatible with PHP 8.1 by consuming the declarediterablereturn type without assuming aTraversable; runtime behavior and the public SDK API are unchanged. -
v1.20.1— Adds the internal versioned RunEvent/Checkpoint contract and SQLite Claim/Lease/Fencing recovery path without changing the public SDK API. RealSIGKILLprocess tests verify fail-closed mutation recovery; exported events are redacted by default, and Agent/Model/Tool spans reuse canonical RunJournal identities for trace correlation. -
v1.20.0— Completes the existing architecture-convergence queue without adding a workflow DSL: coding and generic context presets are explicit; tool results and registry identities have one validated runtime contract; run limits, agent invocation, message visibility, prompt sensitivity, and provider cache adaptation each have a single internal authority. Existing defaults and provider request shapes remain compatible. -
v1.19.10— Adds a no-waiver 500-line limit for every tracked PHP file, enforced by Composer and Linux/Windows CI. Existing oversized production and test classes are split into responsibility-oriented concerns without changing the SDK public API snapshot or runtime behavior. -
v1.19.9— Host and sandboxReadtools now share one bounded text scanner for line windows, cancellation, hashing, and size failures; failed or aborted reads never create write-authorizing receipts. Coding prompt context is also isolated behind one internal preset while text-only calls keep their minimal no-tools prompt and existing public configuration contract. -
v1.19.8— Runtime provider switching now treats provider type, credentials, model, endpoint, token limits, capability checks, and cost context as one validated identity. The SDK preflights the actual tool and sandbox manifest, rolls rejected setting changes back atomically, and runs all three provider adapters through the same success and fault conformance matrix. -
v1.19.7— AgentRun and Tokimo bound command-response transport data before JSON decoding. Oversized remote/runner payloads now use the establishedoutputLimitedresult contract instead of accumulating in PHP memory, and a Tokimo runner cleanly restarts after that boundary while still accepting ordinary chunked JSONL responses. -
v1.19.6— AConversationloaded throughHaoCode::resume()now cleans its unrelated fresh sandbox after a streaming resume pauses again, while the durable checkpoint sandbox remains available for the next decision. -
v1.19.5— AConversationloaded withHaoCode::resume()now reclaims the durable interrupt checkpoint's sandbox lease before rebuilding afterresumeInterrupt()orstreamResumeInterrupt(). Follow-up tool calls keep seeing pre-interrupt files instead of a fresh sandbox. -
v1.19.4— Cancellation now has one consistent lifecycle boundary across ordinary, parent/child, event-pump, and tool-turn-limit finalization paths. A response that completes after cancellation is not persisted or reported as a completed agent run. -
v1.19.3— Tool scheduling now preserves stateful execution barriers: a later read-only call cannot run ahead of a preceding mutation, including in streaming early execution. Compatible provider streams also retain text, thinking, and complete tool arguments supplied in a content-block start event, while standard empty tool-input placeholders still accept later JSON deltas. -
v1.19.2— One-shot Runner and facade streams now release their owned runtime resources before exposing terminal results or errors, so retaining a completed Generator cannot keep a sandbox, MCP connection, or abort listener alive. Re-interrupting streaming resumes preserve the durable sandbox lease. -
v1.19.1— Completes compatible OpenAI Chat streams that explicitly end in[DONE]but omitfinish_reason, while preserving partial-response recovery for bare EOF. Terminal conversation stream messages release their operation lease before they are yielded, so a retained completed Generator cannot block or later corrupt an immediate follow-up or interrupt resume. DurableConversationresumes also transfer the reattached sandbox lease to their rebuilt handle, preserving pre-interrupt files for the next tool call. -
v1.19.0— Hardened existing agent lifecycle contracts: every SDK result surface reports current-turn context usage; durable and streaming interrupt resumes preserve cumulative usage, scoped Skill models, and canonical session lifecycle without replaying first-turn context/hooks; early tool completion events reflect worker completion; malformed gateway usage cannot decrease totals or cost. -
v1.11.0— Streamable HTTP MCP sessions (incremental SSE, reverse RPC, recovery, OAuth, cooperative event polling), and reduced repeated Git/memory/ tool-schema work in long-running sessions. -
v1.11.1— Cache-stable system prompts, tool schemas, and conversation history with volatile Git context moved into the initial user turn; normalized DeepSeek and OpenAI-compatible cache usage accounting. -
v1.12.0— Native smart HITL modes (HitlPolicy,HitlReviewer,SmartInterruptDecider) with rule-based fast paths and a guardian review model. -
v1.12.1— Smart HITL aligned with codex semantics: recursive command grading, default mode'smart', sandbox-containment fast path, and thehitlAllowlistPath"always allow" file. -
v1.13.0— Prefix-based always-allow rules (every segment of a compound command must match) and temp-dir redirect downgrade from red-line to gray review. -
v1.13.1— Session JSONL writes never kill or corrupt a run: invalid UTF-8 and non-finite doubles are sanitized with partial-output fallback. -
v1.18.6— Durable HITL preserves inline Skill scopes, cumulative budgets, multimodal content, and managed worktree lifecycle; provider model selection, pricing, transcript writes, and runtime reset safety now fail closed. -
v1.18.7— Provider connections resolve credentials and model limits as one vendor-safe unit; adaptive thinking, shared run-tree budgets, consistent HITL resumes, worktree context, and durable session writes are hardened. -
v1.18.8— UnknownproviderTypefail-closed; skillBash(pattern)capability rules enforced without silent widening; skill model aliases provider-aware; session resume restores transcript cwd;interrupt_failedterminal state; durable structured retries share one conversation; branch refuses unfinished HITL; budget documented as a shared post-response guard with stricter resume limits; adaptive effort mapped fromthinkingBudget/effort_level. -
v1.18.9— Skill Bash patterns reject shell chaining/expansion (not just prefix match);Conversationreload after interrupt resume restores session / live-run cwd; post-claim HITL failures always recordinterrupt_failed. -
v1.18.10— Polish existing tools: drop misleading BashdangerouslyDisableSandbox; harden background Bash bookkeeping (TTL, start-token, harvest-on-check); MCPlist*followsnextCursor; Edit/Patch refuse binary/NUL payloads. -
v1.18.11— Contract fixes: background Bash no double-run + real exit codes;SdkTool/AgentAsTooldefault non-read-only; AgentAsTool inherits parent cwd/budget/abort and rethrows HITL; Runner inherits Agent ephemeral; invalidhitlModethrows; budget ledgers may only tighten; corrupt memory files fail closed; Edit tolerates missing fileinfo; structured retries on bad JSON;turnsUsedis per-operation; MCP list uses one total deadline. -
v1.18.12— Structured retries always share one Conversation (even ephemeral) with correction-only turns; HITL structured resume re-validates schema; AgentAsTool accepts any parentLlmProviderand parent ToolRegistry; durable HITL keeps sandbox roots across interrupt/resume; Bash/LocalSandbox use process-group kill + unified env denylist; nested agent tokens roll into parent usage; MCP connect/initialize share one absolute deadline. -
v1.18.13— Close cross-layer contracts: AgentRun/Tokimo durable leases without secrets; lease identity vs caller policy; interrupt JSONL corruption fail-closed; sandbox reserved tool names; shared usage in HITL snapshots; structured correction disables tools; scalar schema rejected early; MCP init full reset; Sandbox Bash honors abort during exec; background Bash watchdog. -
v1.18.14— Nested agent cost joins shared run accounting without losing per-child deltas; custom ExecPolicyenv_denyapplies consistently to foreground, background, and sandbox Bash; invalid HITL decisions preserve pending sandbox leases; structured streaming resume and MCP deadlines fail closed; release lint now covers examples. -
v1.18.15— SDK lifecycle and persistence contracts are hardened: durable HITL sandboxes survive non-streaming and repeated-interrupt paths; abandoned streams abort and release execution state; hooks time out, fail closed, and revalidate modified input; committed credential streams are never replayed; settings and session reads fail closed; parallel tool results retain metadata and outcomes. -
v1.18.16— Local file tools enforce text-onlyReadboundaries, complete and current revision receipts, and atomic conflict-safe mutation; context compaction preserves tool-call/result structure;Conversation::loadSession()atomically replaces existing in-memory history; CI discovers and runs every non-live test. -
v1.18.17— Cross-platform release gates now cover native Windows path and glob behavior, PHP 8.1–8.3 stream abandonment, clean-checkout sandbox tests, fail-closed Hook working directories, and Windows-form sensitive paths. -
v1.18.18— Same-responseRead+Writebatches no longer satisfy read-before-write authorization; internal Git diffs and worktree commands use isolated argv-based execution; worktree entry/exit preserves session cwd without trusting hooks, symlinks, or the PHP process cwd. -
v1.18.19—ExitWorktreeis now classified as non-read-only because both keep and remove actions change session working-directory state. -
v1.18.20— Background Bash launches return promptly, supervise timeout from a detached PHP worker, terminate the command tree before writing exit 124, and keep captured background output under a physical byte cap. -
v1.18.21— MCP stdio requests now write stdin, read stdout, and drain stderr under one deadline, preventing stderr pipe pressure from deadlocking normal JSON-RPC responses and closing stdio process trees on shutdown. -
v1.18.22— Parallel tool execution avoids early/parallel forks whenPreToolUsehooks may rewrite input, caps forked workers, and makes forked tool children process-group leaders so cleanup cancels descendant processes. -
v1.18.23— Grep streams ripgrep output and stops after the requested global result window, the PHP fallback scans files line-by-line, and Glob bounds brace expansion, visited files, ignored heavy directories, and retained top results. -
v1.18.24— Foreground Bash captures stdout/stderr through bounded endpoints, terminates runaway output at the byte cap, and still returns promptly when a command leaves its own background child running. -
v1.18.25— Text Read streams line windows while computing file revisions, segmented reads merge line coverage into complete receipts only after model-visible batch commit, and Edit/Write size checks now bound replacement payloads rather than rejecting large files outright. -
v1.18.26— WebFetch now requests and returns only explicit text-like media types, rejects binary or missing Content-Type responses before streaming body bytes, and normalizes fetched text to valid UTF-8 before tool results/cache. -
v1.18.27— File Read rejects extreme single-line buffers, runs PDF text extraction through argv-based bounded subprocesses with page validation, timeout, and output caps, and bounds notebook input/rendered output receipts. -
v1.18.28— Internal unified diffs are generated in PHP without relying on the hostdiffcommand, and Bash-backed process startup now reports a clear missing-bashdiagnostic before attempting command execution. -
v1.18.29— The Windows release gate now runs the internal diff and local process startup contract tests in addition to path compatibility checks. -
v1.18.30— Hardened Git subprocesses preserve exit codes captured byproc_get_status()so PHP 8.1/8.2 CI does not drop successful internal diff output afterproc_close()returns-1. -
v1.18.31— PDF text extraction now honors the active tool abort signal, terminates the extraction process tree, and reports interrupted reads without recording a file-read receipt. -
v1.18.32— Automatic Git diff generation after Edit/Write now carries the active tool abort signal into HardenedGitRunner so interrupted runs stop the internal Git process tree promptly. -
v1.18.33— Release metadata now points at the current audited source line so package consumers can distinguish this published patch tag from the prior hardening releases. -
v1.18.34— Git prompt context, managed agent worktrees, file-history diffs, and local executable discovery now avoid shell wrappers, carry bounded argv execution, and keep worktree cleanup tied to the owning repository. -
v1.18.35— Skill registry Git sync now reuses the shared hardened Git runner, and LSP server startup uses argv-based, bounded non-blocking stdio with process-tree cleanup. -
v1.18.36— Glob and Grep now prune default ignored directories before descending into them, and search paths honor abort signals across ripgrep and PHP fallback execution. -
v1.18.37— Local sandbox Glob and Grep now prune default ignored directories before descending, cap retained glob results, stream grep line reads, and preserve sandbox search abort and zero-limit semantics. -
v1.18.38— Local sandbox Read now streams text line windows while hashing the full file revision, avoids caching full file contents for partial reads, rejects extreme single-line buffers, and honors abort signals. -
v1.18.39— Local sandbox Bash now captures stdout and stderr through bounded pipes, terminates commands that exceed the capture cap, and reports output-limit metadata instead of allowing unbounded temporary output files. -
v1.18.40— Native, Tokimo, and AgentRun sandbox exec paths now share theoutputLimitedmetadata contract; native sandbox commands are terminated when captured output exceeds the backend cap, while remote sandbox responses are bounded before reaching tool results. -
v1.18.41— AgentRun sandbox exec now returns the same non-zero exit status as other sandbox backends when SDK-side output bounding marks a commandoutputLimited. -
v1.18.42— Optional cron daemon job execution now uses the shared process supervisor, drains stdout and stderr under a single timeout, and terminates descendant processes before delayed side effects can run. -
v1.18.43— Cron daemon jobs now keep that supervised execution path without starting a login shell, so per-user shell profiles cannot bypass the cron environment allowlist or add hidden startup side effects. -
v1.18.44— Worktree creation now rejects.claude/worktreessymlinks, verifies the worktree base remains inside.claude, and updates.gitignorethrough the atomic writer instead of a direct append. -
v1.18.45— Durable HITL checkpoints now preserve pending Read receipts and promote them only after the resumed batch's tool results become model-visible, so approved same-batch writes cannot borrow unread content while the next turn can safely retry after seeing the Read output. -
v1.18.46— Parallel and streaming-early tool forks now cap serialized IPC payloads before writing or reading temp files, preventing oversized metadata from bypassing tool-result output caps while preserving aborted/error outcomes. -
v1.18.47— Release metadata refresh for the verifiedv1.18.46hardening source line, keeping downstream package indexing aligned with the current audited commit. -
v1.18.48— Host Grep PHP fallback and Glob now honor root.gitignorerules before returning or descending into ignored files, reducing duplicate worktree/vendor search cost while preserving bounded traversal semantics. -
v1.18.49— Fallback search and MCP process boundaries now reject pathological traversal, validate cross-platform roots, and close stale stdio descendants during reconnect and shutdown. -
v1.18.50— Large-file Read/Edit/Write paths, foreground/background Bash, parallel tool cancellation, Grep/Glob resource limits, and Windows path normalization now fail closed with bounded work and regression coverage. -
v1.18.54— Provider, MCP, and sandbox error bodies are now read with bounded prefixes; SSE lines and multiline events reject oversized input; stdio rejects newline-terminated oversized frames; and fallback search resource limits include literal Glob patterns and retained Grep context.CredentialPoolrejects non-finite or negative exhaustion TTLs, andStructuredResult::toJson()now fails withJsonExceptioninstead of returning an invalid JSON value. -
v1.18.57— Native OpenAI Chat streams retain rate-limit headers for credential-pool backoff, recent-session selection uses mtime plus a bounded header scan, and MCP/AgentRun lifecycle and write-safety contracts are documented. Retry, snapshot, parallel IPC, Skill scope, JSONL, and background Bash bookkeeping were split into internal helpers without changing the public SDK surface. -
v1.18.58— Read-receipt batch promotion is isolated behind an internal lifecycle helper. Sandbox Glob keeps the host result heading and explicit bounded-result notice, while sandbox search failures use tool-specific prefixes. -
v1.18.59— Sandbox Glob's bounded-result wording no longer implies an unknown total count; it remains explicit without overstating the result. -
v1.18.60— AgentLoop rejects invalid zero or negative turn budgets and stops repeating identical valid tool-error batches with a final no-tool request; provider fallback retries add bounded jitter while honoring explicitRetry-Afterdelays, with the lifecycle and deployment contracts documented and covered by regression tests. -
v1.18.56— AgentRun Glob/Grep now prune and bound remote searches, preserve path-level glob semantics, and expose residual-difference metadata; Tokimo and AgentRun abort waits mid-run, and all sandbox exec backends use the aligned 100,000-byte output-limit contract. -
v1.18.55— Stream text/thinking accumulation, Bash result metadata, and context-compaction thresholds now share tighter fail-closed contracts; sandbox and MCP documentation were refreshed for the current implementation. -
v1.18.53— Preserve quoted Bash command arguments on Windows, validate and normalize inputs before parallel/early execution, and bound unterminated MCP SSE metadata before buffering it. -
v1.18.52— Remaining tool resource boundaries now clean up forked process trees and IPC files on callback failures, host search honors ancestor and nested.gitignorerules, and parallel safety classification uses normalized inputs while failing closed around permission prompts. -
v1.18.51— Extensionless large text files no longer inherit a false binary classification from PHP'sapplication/octet-streamMIME fallback.
License
MIT