Skip to content

Claude Code Runner ​

The Claude Code runner invokes the claude CLI in non-interactive print mode (claude -p) with --output-format stream-json.

Requirements ​

  • Claude Code CLI >= 2.1.0
  • The claude binary must be on your PATH (or specify a custom path)

How It Works ​

The runner spawns claude -p --output-format stream-json as a subprocess and parses the newline-delimited JSON stream. Each line is a typed message (system, assistant, tool_use, tool_result, result, error).

Runner-Specific Options ​

OptionTypeDescription
allowedToolsstring[]Tools the agent may use
disallowedToolsstring[]Tools the agent may not use
toolsstring[]Exclusive built-in tool whitelist (see below)
permissionModestringPermission mode for the run (see below)
mcpConfigstringPath to MCP server configuration file
settingsstringAdditional settings: file path or inline JSON (see below)
jsonSchemastringJSON Schema for structured output
maxBudgetUSDfloatCost limit in USD
resumestringSession ID to resume
continueboolContinue the most recent session
appendSystemPromptstringAppended to the default system prompt
includePartialMessagesboolStream partial/incremental messages
channelEnabledboolEnable two-way channel communication
channelLogFilestringFile path for channel server logs
channelLogLevelstringChannel server log level (debug, info, warn, error)

Stream Message Types ​

TypeDescription
systemInit message with model, session_id, tools
assistantText, thinking, or tool_use content
userTool results
resultFinal result with cost, usage, duration
stream_eventRaw API streaming events
channel_replyReply from the agent to a channel message
rate_limit_eventRate limit notifications

Examples ​

Simple Run ​

go
runner := claudecode.NewRunner()
result, err := runner.Run(ctx, "Summarize the README")
ts
const runner = createClaudeRunner();
const result = await runner.run("Summarize the README");
python
runner = ClaudeRunner()
result = await runner.run("Summarize the README")

With Tool Restrictions ​

go
result, err := runner.Run(ctx, "Read the config file",
    claudecode.WithAllowedTools("Read"),
    claudecode.WithDisallowedTools("Write", "Bash"),
)
ts
const result = await runner.run("Read the config file", {
  allowedTools: ["Read"],
  disallowedTools: ["Write", "Bash"],
});
python
result = await runner.run("Read the config file", ClaudeRunOptions(
    allowed_tools=["Read"],
    disallowed_tools=["Write", "Bash"],
))

Exclusive Tool Whitelist ​

allowedTools only pre-approves tools — unlisted built-ins remain registered and can still execute. tools maps to the CLI's --tools flag, which replaces the built-in tool set: every unlisted built-in is denied at registration, including built-ins introduced by future CLI versions. It acts independently of the permission system, so denied tools stay denied even with dangerouslySkipPermissions.

Pass "" to disable all tools, or "default" to restore the full built-in set. Omitting the option leaves the CLI default in place.

go
result, err := runner.Run(ctx, "Read the config file",
    claudecode.WithTools("Read", "Grep"),
)
ts
const result = await runner.run("Read the config file", {
  tools: ["Read", "Grep"],
});
python
result = await runner.run("Read the config file", ClaudeRunOptions(
    tools=["Read", "Grep"],
))

Permission Mode ​

dangerouslySkipPermissions is all-or-nothing — it bypasses every prompt. permissionMode maps to the CLI's --permission-mode flag and selects a graduated mode instead. Known modes:

ModeBehavior
defaultPrompt for permission on first use of each tool
acceptEditsAuto-accept file edits, prompt for everything else
autoAuto-approve without prompting
planPlan only — no tool execution
dontAskNever prompt; deny anything not pre-approved
bypassPermissionsBypass all permission checks

The value is passed through as a plain string rather than a closed enum, so new CLI modes work without a library upgrade.

When both options are set, permissionMode wins and --dangerously-skip-permissions is omitted. Setting only dangerouslySkipPermissions still passes the skip flag as before.

go
result, err := runner.Run(ctx, "Draft a refactor",
    claudecode.WithPermissionMode("plan"),
)
ts
const result = await runner.run("Draft a refactor", {
  permissionMode: "plan",
});
python
result = await runner.run("Draft a refactor", ClaudeRunOptions(
    permission_mode="plan",
))

Additional Settings ​

settings maps to the CLI's --settings flag and loads extra configuration for a single run — permission blocks, hooks, env, and so on — without staging a whole CLAUDE_CONFIG_DIR. The value is either a path to a settings JSON file or a raw JSON string; it is passed through verbatim and the CLI disambiguates.

WARNING

In non-interactive (-p) mode the CLI silently ignores settings that fail validation — no error is raised and the run proceeds without them. Do not rely on a malformed path or JSON string surfacing an error; validate the value yourself if it matters.

go
result, err := runner.Run(ctx, "Audit this module",
    claudecode.WithSettings(`{"permissions": {"deny": ["Bash"]}}`),
)
ts
const result = await runner.run("Audit this module", {
  settings: `{"permissions": {"deny": ["Bash"]}}`,
});
python
result = await runner.run("Audit this module", ClaudeRunOptions(
    settings='{"permissions": {"deny": ["Bash"]}}',
))

Session Resume ​

go
// First run
result, _ := runner.Run(ctx, "Start a task")

// Resume later
result, _ = runner.Run(ctx, "Continue",
    claudecode.WithResume(result.SessionID),
)
ts
const result = await runner.run("Start a task");
const resumed = await runner.run("Continue", {
  resume: result.sessionId,
});
python
result = await runner.run("Start a task")
resumed = await runner.run("Continue", ClaudeRunOptions(
    resume=result.session_id,
))

Streaming with Partial Messages ​

go
session, _ := runner.Start(ctx, "Write a story",
    claudecode.WithIncludePartialMessages(),
)
for msg := range session.Messages {
    fmt.Print(msg.Text())
}
ts
const session = runner.start("Write a story", {
  includePartialMessages: true,
});
for await (const msg of session.messages) {
  const delta = messageTextDelta(msg);
  if (delta) process.stdout.write(delta);
}
python
session = runner.start("Write a story", ClaudeRunOptions(
    include_partial_messages=True,
))
async for msg in session:
    if msg.text:
        print(msg.text, end="")