Skip to main content
Reference

Troubleshooting

Match the symptom or exact message you see to its cause and fix.

This page is indexed by what you observe. If you already have an error code, an exception class, or a process exit code, go to Errors and error codes instead — that page is indexed by error identity.

The session never starts

Symptom: query() yields nothing, or the first await hangs and eventually throws.
CauseHow to confirmFix
The qoderclicn executable is not on PATHThe error is QoderCliProcessError and mentions the executableSet pathToQoderCLIExecutable, or export QODERCLI_PATH
Startup exceeded the load timeoutFailure occurs after a consistent delayRaise loadTimeoutMs; a cold start on a slow disk can exceed the default
No credential was configuredError carries code: 'auth_not_configured'Pass auth; see Authentication
The credential env var is unsetcode: 'auth_access_token_env_var_not_configured' or ..._service_account_...Export the variable the auth helper reads
Capture stderr while diagnosing startup — it usually names the real problem:
options: {
  stderr: (data) => process.stderr.write(data),
}

An option is rejected before the process starts

The SDK validates several options up front, so a typo fails fast instead of silently disabling a feature. These are thrown synchronously, before QoderCN CLI is started.
MessageCauseFix
securityScan contains unknown option: <key>
security_scan contains unknown option: <key>.
Misspelled switch nameTypeScript uses l1StaticCheck / l2LightweightScan / l3DeepScan; Python uses l1_static_check / l2_lightweight_scan / l3_deep_scan
securityScan.<key> must be a boolean
security_scan.<key> must be a bool.
A string such as 'yes' was passedPass true or false
security_scan must be a dict.A non-mapping value was passed in PythonPass a dict of switches
native mode requires at least one enabled scope; use memory: {} to disable SDK memoryBoth projectScope and userScope set to false (TypeScript only)Keep one scope, or use memory: {} to turn memory off
[createSdkMcpServer] Server name must be a non-empty string.Missing or blank nameGive the server a name
[createSdkMcpServer] Tool name must be a non-empty string in server "<server>".A tool was defined without a nameName every tool
[createSdkMcpServer] Tool "<tool>" must have a non-empty description string.Missing descriptionAdd a description; the model relies on it to decide when to call the tool

The SDK and runtime disagree

Symptom: ProtocolVersionMismatchError or UnsupportedCliCapabilityError. These mean the SDK and QoderCN CLI are from incompatible releases, which normally happens when the runtime is managed separately instead of using the one bundled with the package.
try {
  for await (const message of query({ prompt: 'ping' })) { /* ... */ }
} catch (error) {
  if (error instanceof UnsupportedCliCapabilityError) {
    console.error(`runtime lacks capability: ${error.capability}`);
  }
}
UnsupportedCliCapabilityError.capability names the exact missing feature, which tells you whether to upgrade the runtime or stop using that option. Upgrade QoderCN CLI, or remove the pathToQoderCLIExecutable override so the bundled runtime is used.

The agent never calls my custom tool

Symptom: The tool is registered, the session starts, and the agent solves the task without it. Work through these in order:
  1. Check the fully-qualified name. A tool named greet on a server named my_tools is mcp__my_tools__greet to the model. Allowlists must use the full name.
  2. Check whether an allowlist excludes it. If tools or allowedTools is set and omits the tool, the model never sees it. See Tool naming and allowlists.
  3. Check lazy loading. With MCP lazy loading enabled, tools are not all resident. Mark the ones that must always be available:
tool('greet', 'Say hello', { name: z.string() }, handler, { alwaysLoad: true })
  1. Check the description. The description is the model's only signal for when the tool applies. "Does a thing" gets ignored; state the trigger condition.

The agent stops on a permission it should have

Symptom: Tool calls are denied, or the run stalls waiting for approval that never arrives.
CauseFix
disallowedTools overlaps the tooldisallowedTools takes priority over allowedTools and permissionMode
No approval handler in a non-interactive jobSet a permissionMode that does not require prompts, or supply canUseTool
Both canUseTool and permissionPromptToolName setThey are mutually exclusive; choose one
See Permission Control for the full precedence order.

Model selection times out

Symptom: ModelPolicyTimeoutError. resolveModel runs on the critical path of every request and has a 500 ms default budget. A callback that queries a database or an HTTP service will exceed it.
options: {
  resolveModel: async (context) => ({ model: pickModel(context) }), // keep it local
  resolveModelTimeoutMs: 1_500,                                     // or raise the budget
}
Cache the decision outside the callback rather than raising the timeout repeatedly — every millisecond here is added to every turn.

Memory was not written

Applies to TypeScript only — the memory option does not exist in the Python SDK. Symptom: The agent learned something, the run finished, and nothing was saved. Generation runs in a background agent after the turn completes. A process that exits immediately after the final result message can terminate it mid-write.
// Wait for pending generation before exiting
await q.flushMemory();
If flushMemory() returns and files are still unchanged, inspect the generation result — no_change and skipped are normal outcomes, not failures. See Give agents persistent memory.

A feature I enabled in settings is off

Symptom: A settings file enables something, but the session behaves as if it were disabled. Some query options are authoritative: when provided, they replace the corresponding settings block rather than merging with it, and any switch you omit becomes false. securityScan behaves this way. If settings.json enables l1StaticCheck and the query passes securityScan: { l3DeepScan: true }, then l1StaticCheck is off for that session. Restate every switch you want enabled. See Scan code for security issues.

The agent follows rules I never set

Symptom: The agent obeys conventions that are not in your prompt. Instruction files on the machine running QoderCN CLI are loaded by default, and they can include other files. Log what actually loaded:
options: {
  hooks: {
    InstructionsLoaded: [{ hooks: [async (input) => {
      console.log(input.memory_type, input.file_path, input.load_reason);
      return {};
    }] }],
  },
}
For reproducible runs across hosts, load nothing from disk with settingSources: []. See Customize the system prompt.

Capturing a report worth filing

When you need to escalate, collect these before the state is lost:
options: {
  debug: true,
  debugFile: '/tmp/qoder-sdk-debug.ndjson',
  stderr: (data) => process.stderr.write(data),
}
Include the SDK version, the QoderCN CLI version from the system/init message, the session_id, and the Result subtype with any error_code. Redact credentials, prompts, and source content unless the diagnostic explicitly requires them — see Logging and support.

Next steps