Field-by-field reference for ~/.coddy/config.yaml. For narrative documentation (file discovery, .env, provider guides) see Configuration.
A machine-readable JSON Schema accompanies this reference, published at https://coddy.dev/config.schema.json and embedded into the binary, where coddy -t / --test-config checks a file against it from the command line (see Configuration). Point your editor's YAML language server at it to get autocomplete and typo checking:
# yaml-language-server: $schema=https://coddy.dev/config.schema.jsonVS Code (with the YAML extension), Zed, Neovim and Helix pick this comment up automatically, and Coddy writes it into every config.yaml it saves (see Configuration); JetBrains IDEs do not read it and need the URL registered under JSON Schema Mappings instead. The schema is kept in sync with the Go config structs by TestDocsConfigSchemaMatchesStructs in internal/config/docs_schema_test.go. Optional tri-state fields (for example compaction.enable, models[].stream, tools.output_limits.*) accept null as well as a value: null means unset, exactly like an omitted key. When Coddy saves the file it simply leaves unset fields out - it never writes null itself, and it adds a key only when its value differs from the built-in default, so a hand-kept file stays sparse.
Every field is optional unless marked required; an empty config.yaml (or none at all) is valid and uses built-in defaults. Any string value may reference environment variables with ${VAR_NAME} (expanded when the file is loaded). To keep a literal $ in a value (e.g. a secret like $2y$10$…), double it as $$ - the UI does this automatically for the proxy fields. ${CODDY_HOME} is expanded by the loader; ${CWD} stays in the loaded value and is expanded per session by whatever reads the path, except in the process-scoped sessions.dir, memory.dir, and logger.file (see Configuration).
Agent sessions expose a typed configuration tool family with staged, uci-like semantics:
config_getreads a dotted path from the active YAML file. Secret-shaped fields (includingapi_key_commandand any key ending in_api_key, such astools.websearch.brave_api_key) and environment and header values (anenvorheadersentry, adefault_headersvalue) are returned as<redacted>; the MCP server list of the HTTP API shows the values of both mcp.json files the same way.config_setstages UCI-style commands (set,add_list,del_list,delete) without touching the file. Unknown schema paths and commands that would make the config invalid are rejected at staging time. Echoed command lists mask secret-shaped values as<redacted>; the staged store keeps the original values.config_changeslists the staged commands that a commit would apply (secrets redacted).config_commitapplies the staged batch: validates, snapshots the previous file toconfig.yaml.prev(an empty document when the config file did not exist yet, so the first commit stays reversible), writes atomically, and hot-reloads skills, rules, built-in tools, and the MCP trust policy. Because a commit can start MCP processes (a changedmcp.project_trust) and change the permission policy itself, it prompts for tool permission in bothaskandaccept_editsmodes - only a session inbypassskips the dialog - and the prompt lists the staged commands with secrets redacted. The agent is additionally instructed to ask the user to confirm saving first. If runtime reload fails, the file is restored and the staged commands are kept; if even that restore fails, the staged list stays consumed so a blind retry cannot replay it.config_revertdiscards staged commands (all of them, or those under one path).config_rollbackrestores the pre-commit snapshot over the active file (swapping the two, so a second rollback undoes the first) and hot-reloads. It carries the same permission policy asconfig_commit, and the agent warns the user before calling it.
Commands and paths are dotted like OpenWrt's uci CLI, with a selector for named sequence entries:
| Command | Meaning |
|---|---|
set agent.max_turns=40 |
Set a mapping field |
set providers[name=openrouter]={"type":"openai"} |
Select a sequence object by scalar field; append it when setting if absent |
add_list skills.dirs=/opt/skills |
Append a sequence entry |
del_list skills.dirs=/opt/skills |
Remove a matching sequence entry |
delete providers[name=openrouter] |
Delete a field or entry |
skills.dirs.0 (path form) |
Sequence index |
The root path (. or /) is read-only. Values are JSON for objects and arrays; string-typed fields take the literal text. Staged commands persist in the session bundle, so they survive restarts and HTTP permission resumes.
The bundled /configure-coddy skill teaches the agent this syntax, the confirm-then-commit workflow, and the safe discovery/install workflow for MCP servers and skills; it also carries the agent-facing catalog of configuration areas and must be updated together with this reference on any schema change. Process-level listener changes may still require restarting the relevant command; the hot reload is specifically guaranteed for the current session's agent configuration, skills, rules, built-in tools, and the MCP trust policy. MCP servers are not part of this file (see mcp).
Generated from internal/config/config.schema.json (the schema embedded into the binary, published at https://coddy.dev/config.schema.json) and from the defaults the loader applies, by make docs; make docs-check fails when this block is stale. Types read as the schema declares them (string or null is a tri-state field), defaults are what an empty config.yaml resolves to, and the description is the doc: line coddy -t prints for the key. Notes that do not fit a table row follow in Notes.
API credentials and transport selection for upstream LLM vendors. When api_key is empty, the runtime reads the NAME_API_KEY environment variable (NAME is the provider name in uppercase with hyphens mapped to underscores); a row of type "codex" signs in with ChatGPT and reads no key.
| Key | Type | Default | Description |
|---|---|---|---|
providers |
list of objects | API credentials and transport selection for upstream LLM vendors. When api_key is empty, the runtime reads the NAME_API_KEY environment variable (NAME is the provider name in uppercase with hyphens mapped to underscores); a row of type "codex" signs in with ChatGPT and reads no key. | |
providers[].name |
string | Logical id used as the first segment of models[].model. ASCII letters, digits, hyphen, underscore; must start with a letter. | |
providers[].type |
string, one of openai, anthropic, neuraldeep, opencode, codex, devin, acp |
Wire protocol for this provider. Use "openai" for configurable OpenAI-compatible endpoints (OpenAI, DeepSeek, Groq, Ollama, llama.cpp, LM Studio), "anthropic" for Anthropic, "neuraldeep" for NeuralDeep's OpenAI-compatible API at one of its two official deployments (selected with api_base), "opencode" for the OpenCode Go/Console gateway (account device login or a service account key, its models served over the Responses, Anthropic, Google or OpenAI-compatible protocol per model id and gateway; api_base picks Go or Console), "codex" for ChatGPT OAuth against the official Codex backend (Responses API), "devin" for a Devin (Cognition) account signed in through the browser or the Devin CLI, served by the Devin API server (see https://coddy.dev/docs/features/devin), or "acp" for a local agent that speaks the Agent Client Protocol on stdio (claude-agent-acp, codex-acp, opencode acp, coddy acp), named in the acp block; see https://coddy.dev/docs/features/acp-provider. | |
providers[].api_base |
string | Optional base URL override. For type "openai" include /v1 (e.g. http://localhost:11434/v1); for type "anthropic" an Anthropic-compatible gateway (default https://api.anthropic.com). For type "neuraldeep" it selects the deployment: https://api.neuraldeep.ru/v1 (Russia, the default) or https://api.neuraldeep.tech/v1 (the international mirror); any other value falls back to the default. For type "opencode" it selects the gateway: https://opencode.ai/inference/go/v1 (the Go subscription, the default) or https://opencode.ai/inference/v1 (pay-as-you-go Console); any other value falls back to the default. Ignored for types "codex" and "devin", which always use their official endpoints, and for type "acp", whose agent runs on this machine. | |
providers[].api_key |
string | Provider secret. A literal key, a "${ENV}" reference expanded at load time, or empty to read NAME_API_KEY at LLM call time. Resolution order: api_key -> api_key_command stdout -> NAME_API_KEY. For type "devin" it is a Devin session token and, when set, wins over the browser login and the Devin CLI login. For type "opencode" an explicit key wins over the login coddy providers login opencode stored. Ignored for type "codex", which signs in with ChatGPT, and for type "acp", whose agent signs in on its own. |
|
providers[].api_key_command |
string | Optional credential-helper command. When api_key is empty it runs via the detected host shell (pwsh, powershell, or cmd on Windows; bash or sh elsewhere) and the trimmed stdout is used as the key. On failure resolution falls back to NAME_API_KEY. Never run for types "codex" and "acp", which take no key. | |
providers[].proxy |
string | inherit | inherit (the default, the same as leaving the key out), none, or a proxy URL: http://, https://, socks5:// or socks5h://. inherit follows the proxy the environment of the Coddy process names - HTTPS_PROXY, HTTP_PROXY and NO_PROXY, never for a loopback address; ALL_PROXY is not read. none connects directly and ignores those variables. A proxy URL sends every request of this row through that proxy, NO_PROXY and loopback included; with SOCKS the proxy resolves host names. The setting covers every request of the provider: its completions, its model list, its account usage and its sign-in. For type "acp" the agent makes its own requests: inherit leaves the proxy variables of Coddy's environment as they are, ALL_PROXY included, a proxy URL is passed to it as HTTPS_PROXY and HTTP_PROXY, with NO_PROXY set to the loopback addresses and ALL_PROXY removed, none removes the proxy variables from its environment, and whether the agent honours them is up to the agent. |
providers[].timeout_ms |
integer | 0 | Optional bound on each LLM HTTP request to this provider, including the streamed body read; for type "acp", on each prompt the agent answers. 0 (the default) sets no client timeout, so slow prompt processing on large contexts is never cut short. |
providers[].usage_limits_panel |
boolean or null | true | Show this provider's account usage panel (the console footer line and /usage, the usage section and banner in the web UI) and read the provider's usage endpoint for it (GET /v1/limits for type neuraldeep, the Codex backend's usage endpoint for type codex, the seat-management status RPC for type devin). Omit or true keeps the panel on; false hides it and stops those reads for this row. Only providers whose type has a usage source are affected. |
providers[].acp |
object or null | The agent a provider of type "acp" runs: a local program that speaks the Agent Client Protocol on stdio, started on the first call and kept for every session of the row (one per session with process: per_session, one per workspace when a value names ${CWD}), stopped after idle_timeout_ms without a call. Read only on a row of type acp; see https://coddy.dev/docs/features/acp-provider. | |
providers[].acp.command |
string | The agent program (looked up on PATH), for example claude-agent-acp, codex-acp, opencode or coddy. Required for type acp. | |
providers[].acp.args |
list of strings | Arguments after the command, for example [acp] for opencode and coddy. | |
providers[].acp.env |
map of strings | Variables added to the environment Coddy passes the agent, for example a key the agent reads. ${CWD} in a value, in an argument, in the command or in cwd is the session's workspace, and the agent is then started once per workspace. | |
providers[].acp.cwd |
string | A fixed folder for the agent process and for every ACP session. Empty (the default) opens each session in the workspace of the Coddy session it serves. | |
providers[].acp.auth_method |
string | The id of the authentication method Coddy passes to the ACP authenticate call after initialize, for an agent that refuses a session until a client authenticates it (codex-acp: api-key, with OPENAI_API_KEY in env). Empty calls none: the agent uses the sign-in it has on its own. | |
providers[].acp.permissions |
string, one of ask, allow, deny |
ask | What Coddy answers when the agent asks permission to run one of its tools: ask forwards the request to the person through the session's permission prompt (allowed without asking in a session whose permission mode is bypass, and an edit under accept_edits), allow approves it once, deny refuses it. In the plan and ask modes every request is refused. |
providers[].acp.modes |
object | The agent's own mode (an ACP mode id, such as plan or read-only) for each Coddy session mode. An empty modes.agent keeps the agent's default mode. modes.plan and modes.ask must name a read-only mode of the agent; without it a turn in that mode is refused. | |
providers[].acp.modes.agent |
string | The agent's mode while the session is in agent mode. | |
providers[].acp.modes.plan |
string | The agent's mode while the session is in plan mode. | |
providers[].acp.modes.ask |
string | The agent's mode while the session is in ask mode. | |
providers[].acp.idle_timeout_ms |
integer | 0 | Stop the agent process this long after its last call; the next call starts it again and replays the conversation into a new session. 0 (the default) means sixty minutes. |
providers[].acp.process |
string, one of shared, per_session |
shared | shared (the default): one agent process serves every session of this row, each in an ACP session of its own. per_session: every Coddy session gets an agent process of its own, for an agent that runs one prompt at a time per process (zcode-acp-server). |
Named model entries the agent and UI can select.
| Key | Type | Default | Description |
|---|---|---|---|
models |
list of objects | Named model entries the agent and UI can select. | |
models[].model |
string | "provider_name/api_model_id": the first path segment must match a providers[].name; the remainder is sent to the LLM API (may itself contain slashes). | |
models[].max_tokens |
integer | Upper bound on completion tokens per assistant message. A codex model never sends it (the Codex backend rejects max_output_tokens), so there it bounds nothing, and coddy -t and --dry-run warn about such a value. | |
models[].temperature |
number | Sampling temperature (0 = deterministic; higher = more random). | |
models[].max_context_tokens |
integer | 0 | Context window of the model in tokens: what the web UI context ring, the console context percentage and the automatic compaction trigger measure against. 0 reads it from the provider's model listing when the provider reports one (the NeuralDeep hub, vLLM, OpenRouter, LM Studio), else 128000. |
models[].multimodal |
boolean | false | Model accepts image/file inputs in addition to text; the UI shows a file attachment button for this model, and read shows it the picture in a PNG, JPEG, GIF or WebP file instead of refusing it as binary. |
models[].reasoning_levels |
list of strings or null | Override the reasoning levels offered for this model. Omit to auto-detect from the model id (gpt-5* -> minimal,low,medium,high; OpenAI o-series, gpt-oss*, qwen3*, and Claude extended-thinking models -> low,medium,high), the vendor an aggregator puts in front of it left out (qwen/qwen3.8-flash reads as qwen3.8-flash). An explicit empty list hides the selector. Settings fills this field from GET /coddy/config/reasoning-levels behind its Fetch reasoning levels button. | |
models[].reasoning_default |
string | Reasoning level pre-selected for new chats; must be one of the resolved levels, otherwise ignored. | |
models[].allow_reasoning_off |
boolean | false | Expose Off in the reasoning selector for this model. Enable it only when this provider/model deployment honours Coddy's provider-specific request that disables reasoning; Coddy cannot verify that capability automatically. |
models[].stream |
boolean or null | true | Transport used to talk to this model. Omit (or true) to stream over SSE. false issues one blocking completion request and delivers the whole answer at once, for servers or proxies that handle event streams badly. Rejected for providers of type codex, whose backend is streaming-only. |
models[].toolset |
string | Name of the toolsets[] preset a session on this model is offered its tools by, unless the session picks another with /toolset . Absent or empty offers every tool the mode would. Use it to spare a small-context local model the schemas of tools it never calls. |
Defaults for the main agent loop (model id and safety caps).
| Key | Type | Default | Description |
|---|---|---|---|
agent.queue_mode |
string, one of steer, after_turn |
Preferred queue mode for Enter during a running turn: steer joins the next ReAct step, after_turn starts a new turn after the current answer. When absent, interactive surfaces ask on first use. | |
agent.model |
string | Optional default models[].model id. Interactive surfaces (web UI, console) pick a model per session; unattended calls that name no model (coddy -p, coddy acp, API requests without a model selector) need it set - they report 'no model configured' when it is empty. | |
agent.max_turns |
integer | 165 | Cap on the ReAct steps (LLM calls) of one prompt turn. Defaults to 165; set 0 to disable the cap. A turn that reaches the cap ends with a notice that names it. |
agent.llm_retry_max |
integer or null | 3 | Extra attempts shared by transport retries, empty-answer recovery, first-token re-issues and output-limit recovery (a step cut off at max_tokens) until tool progress or a new follow-up. 0 disables these retries. Loop guards, Stop hooks, fallback models and quota-reset waits have separate limits. |
agent.llm_retry_base_ms |
integer | 1000 | Initial backoff between LLM retries, in milliseconds. A server-provided pause (Retry-After-Ms / Retry-After headers, "Limit resets at" / "retry in Ns" body phrases) overrides the exponential backoff, capped at 60s. |
agent.llm_min_interval_ms |
integer | 0 | Minimum gap between consecutive LLM calls in milliseconds, retry attempts included (0 disables pacing; e.g. 12000 on strict free tiers). |
agent.llm_first_token_timeout_ms |
integer or null | 90000 | How long a streamed LLM call may stay silent before the turn cancels it (the API hang guard). An explicit 0 disables the guard; blocking (stream: false) transports are never guarded. |
agent.llm_stream_idle_timeout_ms |
integer or null | 300000 | How long a streamed LLM response may send nothing after its first bytes before the stream is cut as stalled (the mid-answer hang guard): the text already delivered is kept and the turn ends with the stall named. An explicit 0 disables the guard; the wait for the first byte belongs to llm_first_token_timeout_ms, and blocking (stream: false) transports are never guarded. |
agent.loop_guard |
boolean or null | true | Runaway-loop protection: cut a streamed response that degenerates into repeating itself, and block a tool called over and over with identical arguments. |
agent.loop_tool_repeat_limit |
integer or null | 2 | Consecutive identical tool calls in successive ReAct responses (same name, same canonical arguments) before the loop guard steps in. 0 disables the tool-repeat check. |
agent.loop_stream_repeat_cycles |
integer or null | 5 | Identical back-to-back output cycles inside one streamed response before the stream is cut. 0 disables the stream check. |
agent.loop_nudge_max |
integer or null | 1 | How many times one turn may be nudged back on track before the loop guard stops it with a notice. |
agent.wait_for_limit_reset |
boolean | false | Wait for a hit usage limit to lift and re-issue the call, instead of ending the turn with the provider's error. Applies to a top-level turn whose provider names a pause beyond the retry budget (a 429 with Retry-After or "Limit resets at"); the turn lock and the client stream stay open while it waits, so it is off by default. |
agent.wait_for_limit_reset_max_ms |
integer or null | 14400000 | Longest time one turn spends waiting for limits in total, in milliseconds, the retry wrapper's own sleeps on a limit included; a pause that would exceed it ends the turn at once with the error. An explicit 0 never waits. |
Override the built-in system prompt templates (Go text/template).
| Key | Type | Default | Description |
|---|---|---|---|
prompts.dir |
string | "" | Directory containing prompt template files. Empty uses embedded defaults. Supports ~ and ${CWD} (session cwd at render time). |
prompts.agent_prompt |
string | agent.md | File name inside prompts.dir for agent mode. |
prompts.plan_prompt |
string | plan.md | File name inside prompts.dir for plan mode. |
prompts.ask_prompt |
string | ask.md | File name inside prompts.dir for ask mode. |
Files the operator adds to the system prompt (AGENTS.md convention), after the documents every session reads anyway: the AGENTS.md and DESIGN.md of the agent home, then those of the session folder, and the nested ones of a folder a tool enters, none of which is named here or can be turned off. A file already in the prompt is not read twice. See https://coddy.dev/docs/features/rules.
| Key | Type | Default | Description |
|---|---|---|---|
instructions.files |
list of strings | [] | Extra instruction files, appended after the AGENTS.md and DESIGN.md documents in the order listed; empty by default. ${CODDY_HOME}, ${CWD} and a leading ~ expand; an absolute entry is read as it stands, a relative one resolves against the session CWD. An entry naming a document the prompt already carries is skipped. |
Directories scanned for skills (SKILL.md and root .md/.mdc files).
| Key | Type | Default | Description |
|---|---|---|---|
skills.dirs |
list of strings | [] | Extra skill directories, read after the four default folders and stronger than them. The defaults are always read, lowest priority first: ${HOME}/.agents/skills (shared with every agent, npx skills and npx skillsbd install there), the project's .agents/skills, ${CODDY_HOME}/skills (Coddy's own and installed skills), the project's .coddy/skills; then these entries in their order. A skill found in several directories is taken from the last one in this order, and a directory named twice is read at its last place. ${CODDY_HOME} expands when the file is loaded, ${HOME} and ~ to your home folder, ${CWD} and a relative path against the session's workspace (the folder a new chat picked included). |
skills.project_trust |
string, one of ask, allow, deny
|
ask | Trust policy for the project's .coddy/marketplaces.json, which travels with the checkout: "ask" (default) leaves its sources and marketplaces out of every sync until the operator approves that exact entry for that workspace; "allow" treats them like the operator's own ${CODDY_HOME}/marketplaces.json; "deny" never uses them (they are listed as switched off). What they install goes to ${CODDY_HOME}/skills. The project's skill folders (.coddy/skills, .agents/skills) are not affected. Skill marketplaces are not declared in config.yaml: they live in ${CODDY_HOME}/marketplaces.json and the project's .coddy/marketplaces.json, and an old skills.sources list is moved into the home file on load. See https://coddy.dev/docs/features/skills#project-marketplaces-and-trust. |
skills.auto_discovery |
boolean or null | true | Offer the model-driven load_skill tool so the agent pulls a catalogued skill's full instructions into a turn on its own when the request matches, instead of requiring an explicit /name. Defaults to true. |
Discovery of rule files from ${CODDY_HOME}/rules (the operator's own, applied in every workspace) and from one project folder under the session CWD, the first of .coddy/rules, .agents/rules, .cursor/rules, .claude/rules, .codex/rules that holds a rule file, and the same way under every subfolder on the way down to a file a tool or a mention reaches, each of those rules applying to its own subfolder; .mdc files are Cursor rules, .md files Claude Code rules. The AGENTS.md and DESIGN.md documents are not rules in this sense and none of these settings turns them off. See https://coddy.dev/docs/features/rules.
| Key | Type | Default | Description |
|---|---|---|---|
rules.auto_discover |
boolean or null | true | Scan the rule folders automatically: ${CODDY_HOME}/rules, the project folder of the session CWD and those of the subfolders a tool or a mention reaches. The AGENTS.md and DESIGN.md documents are read either way. |
rules.systems |
list of strings | [] | Restrict which rule systems are loaded: user (${CODDY_HOME}/rules), coddy, agents-dir (.agents/rules), cursor, claude, codex. Empty means all. A project folder left out drops out of the chain; of the ones admitted, the first that holds a rule file is read. agents, which used to switch the nested AGENTS.md files, no longer affects them (they are always read); it is still accepted, and a list holding only agents loads no rule folder. |
MCP settings that are not tied to a single server entry.
| Key | Type | Default | Description |
|---|---|---|---|
mcp.project_trust |
string, one of ask, allow, deny |
ask | Trust policy for the project-local /.coddy/mcp.json, which travels with the checkout: "ask" keeps its servers cold until the operator approves that exact declaration for that workspace; "allow" starts them automatically (trusted workspaces only); "deny" never loads them. |
mcp.idle_timeout_seconds |
integer or null | 300 | Seconds an MCP server that no session holds any more keeps running before Coddy stops it: a project server once the last session of its workspace let it go, a server an ACP client sent once the last session that sent it closed. A session that takes the server in the meantime finds it running. The global servers that coddy serve, the console and coddy acp start with the process are not affected, and a server switched off, no longer approved, removed or declared differently stops at once. 0 stops a server as soon as the last session lets it go. |
Filesystem and shell policy for built-in tools.
| Key | Type | Default | Description |
|---|---|---|---|
tools.command_allowlist |
list of strings | Commands that never require permission. Exact or prefix match (prefix + space + any args). "*" allows all commands. | |
tools.ssh_connect_timeout |
integer | 30 | TCP dial timeout for SSH connections (ssh_run_command tool), in seconds. |
tools.output_limits |
object | Maximum lines each tool result or error may return into the LLM context. Every enabled limit also applies a 64 KiB per-call byte safety ceiling so a huge single line cannot bypass it. 0 disables both limits for that tool. Unset fields fall back to the built-in defaults. | |
tools.output_limits.read |
integer or null | 1000 | Max lines for a read file page or directory listing. |
tools.output_limits.grep |
integer or null | 200 | Max path:line:content records returned by grep. |
tools.output_limits.glob |
integer or null | 300 | Max paths returned by glob. |
tools.output_limits.print_tree |
integer or null | 400 | Max lines of a directory tree. |
tools.output_limits.run_command |
integer or null | 500 | Max combined stdout+stderr lines of a shell command. |
tools.output_limits.ssh_run_command |
integer or null | 500 | Max combined stdout+stderr lines of a remote SSH command. |
tools.output_limits.webfetch |
integer or null | 800 | Max lines of fetched page markdown. |
tools.output_limits.websearch |
integer or null | 200 | Max lines of search results. |
tools.output_limits.default |
integer or null | 1000 | Applies to any tool not named above, including MCP tools. 0 means unlimited. |
tools.background |
object | Commands the agent runs detached in the session task pool instead of blocking a turn. The pool dies with the coddy process; each task's metadata and captured output stay in the session bundle, and a task interrupted by a restart is reported as orphaned. | |
tools.background.enable |
boolean or null | true | Offer the background option on run_command and expose the background task tools. |
tools.background.max_concurrent |
integer | 5 | How many background tasks one session may run at once. 0 uses the default. |
tools.background.default_timeout_seconds |
integer | 900 | Hard limit for a task started without an explicit timeout and without a duration estimate. 0 uses the default. |
tools.background.max_timeout_seconds |
integer | 3600 | Ceiling applied to any requested or estimate-derived timeout. 0 uses the default. |
tools.background.output_buffer_bytes |
integer | 262144 | How much of each task's output stays in memory for the status ticker. The full log is still written to the session bundle. 0 uses the default. |
tools.preview_server |
object | The preview_server tool: a static file server the agent starts over a project directory, on a free port, so the operator can open HTML, JS and CSS work in a browser. The server is a background task of the session and runs until it is stopped, the session is deleted or coddy exits. | |
tools.preview_server.enable |
boolean or null | true | Offer the preview_server tool. The server is a background task, so tools.background.enable: false turns it off as well. |
tools.preview_server.host |
string | 127.0.0.1 | Address the server binds, without a port: the port is always a free one. Anything that is not loopback exposes the served directory to whoever can reach that address. |
tools.preview_server.public_host |
string | "" | Host written into the URL the agent hands out, for a coddy that runs where the operator's browser is not (a container, a remote machine). Empty uses the bind host. |
tools.canvas |
object | The canvas of the web UI: a .canvas file of the workspace shown in the Files window as a board of live frames, each a page of the workspace running in a sandbox, put there by the agent with canvas_show or by the person from the Files window; canvas_inspect reads what the browser saw. See https://coddy.dev/docs/features/canvas. | |
tools.canvas.enable |
boolean or null | true | Offer the canvas_show and canvas_inspect tools, serve frames and show the canvas in the web UI. |
tools.canvas.allow_cdn |
boolean or null | true | Let a frame load libraries, styles and fonts from the common CDNs: cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, cdn.tailwindcss.com, fonts.googleapis.com and fonts.gstatic.com. |
tools.canvas.allowed_sources |
list of strings | Further hosts a frame may load from and connect to, as https://host or https://*.host with an optional port and path. A page on the canvas can send what it reads under its folder to any of them. | |
tools.websearch |
object | Which search engines the websearch tool asks, in what order their results merge, and what it may spend asking them. Each engine reports its own outcome next to the results, so a blocked backend is named rather than counted as "the web has nothing". | |
tools.websearch.engines |
list of strings | ["brave","bing"] | Search engines to ask, in merge order. Unset asks brave then bing. "ddg" and "google" are not asked by default: measured from a server, DuckDuckGo answers every query with an anti-bot interstitial and Google renders its results in the browser. "searxng" needs searxng_url. |
tools.websearch.engine_timeout_seconds |
integer | 8 | Seconds one engine may take before it is reported as unavailable. 0 uses the default. |
tools.websearch.total_timeout_seconds |
integer | 20 | Seconds the whole search may take, however many engines it asks. 0 uses the default. |
tools.websearch.max_concurrent_engines |
integer | 4 | How many engines are asked at once. 0 uses the default. |
tools.websearch.snippet_chars |
integer | 320 | Maximum characters of one result description. 0 uses the default. |
tools.websearch.cache_ttl_seconds |
integer | 300 | Seconds one engine answer is reused before the engine is asked again, so a repeated search does not repeat the request. 0 uses the default; a negative value turns caching off. |
tools.websearch.searxng_url |
string | "" | Base address of your own SearXNG instance, asked over its JSON API (enable the json format in its settings.yml). A self-hosted aggregator is the durable answer to a scraped engine being turned away; localhost and LAN addresses are allowed on purpose. |
tools.websearch.brave_api_key |
string | "" | Brave Search API subscription token. With it the brave engine uses the official JSON API instead of reading the public result page, which has no parser to break when Brave redeploys. Empty reads the BRAVE_API_KEY environment variable (also from ${CODDY_HOME}/.env), so the key need not be stored here. |
tools.http_request |
object | Policy of the http_request tool, the agent's curl: the destinations it reaches without asking and the headers every request sends. Under the ask and accept_edits permission modes a request asks the operator unless its destination is allowed here or was approved in the session; bypass never asks. See https://coddy.dev/docs/features/http-requests. | |
tools.http_request.allowlist |
list of strings | [] | Destinations a request reaches without asking: a host (api.github.com), a subdomain wildcard (.example.com), either with an optional :port, an origin (http://localhost:8080) or an address prefix (https://api.example.com/v1/). "" allows every destination. An entry also covers the files a request uploads and an unchecked certificate; a proxy needs an entry of its own, and a file the response is saved to follows the write policy. |
tools.http_request.default_headers |
map of strings | {} | Headers every http_request call sends unless the call names the header itself: a browser User-Agent for a site that turns tools away, an Accept an internal service expects. They take the place of the tool's own defaults (its coddy-agent User-Agent), a call's headers take the place of them - an empty value in the call removes one - and an empty value here leaves the header out of every call that does not set it. A name is letters, digits, - and _, starting with a letter. Host, Content-Type, Content-Length and Transfer-Encoding describe a single request and are refused, and so are the hop-by-hop headers (Connection, Keep-Alive, Proxy-Connection, TE, Trailer, Upgrade) and Proxy-Authorization, which an https request would carry through the proxy's tunnel to every origin. The headers go to every destination the tool reaches, so a credential here reaches all of them; the permission prompt names them and shows the value only of one that describes the client (User-Agent, Accept...), config_get keeps their values from the model, and webfetch, URL mentions and the model providers never send them. See https://coddy.dev/docs/features/http-requests#default-headers. |
User-defined child agents the model can delegate to with spawn_agent. Definitions are markdown files with YAML frontmatter; each run is a background task of the parent session with its own child session and transcript. See https://coddy.dev/docs/features/subagents.
| Key | Type | Default | Description |
|---|---|---|---|
subagents.enable |
boolean or null | true | Register the spawn_agent tool and list the subagent catalog in the system prompt. |
subagents.dirs |
list of strings | [] | Extra definition directories, read after the four default folders and stronger than them. The defaults are always read, lowest priority first: ${HOME}/.agents/agents, the project's .agents/agents, ${CODDY_HOME}/agents, the project's .coddy/agents; then these entries in their order. A definition found in several directories is taken from the last one in this order, and a directory named twice is read at its last place. ${CODDY_HOME} expands when the file is loaded, ${HOME} and ~ to your home folder, ${CWD} and a relative path against the session's workspace. Directories inside the workspace are project scope and follow the trust policy. |
subagents.project_trust |
string, one of ask, allow, deny
|
ask | Trust policy for definitions found inside the workspace, which travel with the checkout: "ask" loads them but refuses to spawn one until the operator approved that exact file for that workspace (the shield in Settings -> Subagents, coddy agents trust on the machine running coddy, or POST /coddy/subagents/{name}/trust with the session workspace as cwd); "allow" treats them like the operator's own files; "deny" never reads them. |
subagents.max_concurrent |
integer | 4 | How many subagent runs the whole process may have in flight at once, whatever session started them. Starting past the limit is refused, not queued. 0 uses the default. |
subagents.max_depth |
integer or null | 1 | How deep spawning may nest: 1 lets a session spawn subagents that cannot spawn further; 0 forbids spawning everywhere. Omit for the default. |
subagents.default_timeout_seconds |
integer | 1800 | Hard limit for one run whose definition and call give no timeout. Capped by tools.background.max_timeout_seconds. 0 uses the default. |
subagents.max_turns |
integer | 0 | ReAct rounds a child may take. 0 follows agent.max_turns. |
Operator commands run at lifecycle points of a session: before and after a tool call, when a prompt is submitted, when the agent stops, on session start and around compaction. Definitions are JSON files in the Claude Code shape (~/.coddy/hooks.json, .coddy/hooks.json, .claude/settings*.json). See https://coddy.dev/docs/features/hooks.
| Key | Type | Default | Description |
|---|---|---|---|
hooks.enable |
boolean or null | true | Load and run hooks at all. |
hooks.files |
list of strings | ["${CODDY_HOME}/hooks.json","${CWD}/.claude/settings.json","${CWD}/.claude/settings.local.json","${CWD}/.coddy/hooks.json"] | Definition files, lowest priority first; every matching hook runs. ${CODDY_HOME} and ${CWD} expand; a relative entry resolves against the session cwd. Files inside the workspace are project scope and follow project_trust; only the hooks key of a Claude Code settings file is read. |
hooks.project_trust |
string, one of ask, allow, deny
|
ask | Trust policy for hook files found inside the workspace, which travel with the checkout: "ask" lists them but runs nothing until the operator approved that exact file for that workspace on the machine running coddy (coddy hooks trust there, or POST /coddy/hooks/trust with the session workspace as cwd); "allow" treats them like the operator's own file; "deny" never reads them. |
hooks.default_timeout_seconds |
integer | 60 | Hard limit for one hook process whose definition gives no timeout. 0 uses the default. |
hooks.stop_loop_limit |
integer | 5 | How many times per turn a Stop hook may send the agent back to work. 0 uses the default. |
hooks.max_output_chars |
integer | 10000 | Cap on the context, messages and reasons one hook may hand to the model or the user; longer values are truncated with a marker. 0 uses the default. |
Process log level, outputs, format, and file rotation.
| Key | Type | Default | Description |
|---|---|---|---|
logger.level |
string, one of debug, info, warn, warning, error |
info | Minimum severity written to the configured outputs ("warning" is accepted as an alias of "warn"). |
logger.levels |
list of objects | Per-component minimum severity. A component is the dotted name a record carries in its "component" attribute (gateway, gateway.telegram, gateway.pachca, session, agent, scheduler, provider.acp); a parent name covers everything nested under it and the longest configured prefix wins. Records with no component follow "level". | |
logger.levels[].component |
string | Dotted component name, for example "gateway.telegram". | |
logger.levels[].level |
string, one of debug, info, warn, warning, error |
Minimum severity for this component. | |
logger.outputs |
list of strings | ["stderr"] | Where log records go; any combination of stdout, stderr, file. Empty means stderr only. |
logger.file |
string | Path for the file sink; required when outputs includes "file". Supports ${CODDY_HOME}. | |
logger.format |
string, one of text, json |
text | Log record format. |
logger.rotation |
object | Size-based rotation for the file sink. | |
logger.rotation.max_size_mb |
integer | 0 | Rotate after the file reaches this size in MB; 0 disables size-based rotation. |
logger.rotation.max_files |
integer | 0 | Rotated backups to keep when max_size_mb > 0. |
Where persisted session bundles are stored.
| Key | Type | Default | Description |
|---|---|---|---|
sessions.dir |
string | "" | Sessions root directory. Empty resolves to ${CODDY_HOME}/sessions. Supports ${CODDY_HOME} and ~. |
Summarizes older conversation history so long sessions keep fitting the model context window. Manual compact command plus automatic trigger at a percent of the model's context window.
| Key | Type | Default | Description |
|---|---|---|---|
compaction.enable |
boolean or null | true | Master switch for compaction (manual command and automatic trigger). Defaults to true. |
compaction.auto_enable |
boolean or null | true | Compact automatically: at threshold_percent, older turns are folded into a summary, or, when the window holds no earlier turn, the earlier steps of the turn being answered (the summary row then starts with the prompt itself, verbatim, and at most the four latest steps stay); and a request the provider refuses as larger than its context window is compacted once and asked again. Set false to keep manual /compact and the Context popover action available without any of this. Defaults to true. |
compaction.threshold_percent |
integer | 80 | Auto-compaction fires when the estimated context usage reaches this percent of the effective model's context window (1..100): its max_context_tokens, else the window its provider's model listing reports, else 128000 - the window the web UI context ring shows. |
compaction.keep_recent_turns |
integer or null | 2 | How many most recent user turns (each with the agent replies and tool activity after it) stay verbatim; only history before that boundary is summarized. 0 summarizes the whole window. When the window holds no more user turns than this, a compaction keeps fewer: the automatic trigger down to the prompt being answered, the manual command down to none. |
compaction.model |
string | "" | Optional models[].model used for the summarization call. Empty uses the session's effective model. |
compaction.fallback_models |
list of strings | Summarizer models tried in order when the one before them fails (models[].model ids). The session's own model is the last resort whether or not it is listed, so a compaction is not blocked by one unreachable summarizer. | |
compaction.result_eviction |
object | Collapses superseded tool results to short placeholders when building the LLM request (the persisted transcript is never rewritten), so paging a large file, a wide search or a fan-out of directory listings cannot pin dead lines in every later turn. Read and grep results survive while the model marks them (keep_result, or keep:true) or they are among the most recent; the listings of glob, print_tree, websearch and webfetch survive within the latest three steps that hold one (a step is one assistant message with every result of its tool calls). A write invalidates earlier reads, greps and file listings that covered what it changed. | |
compaction.result_eviction.enable |
boolean or null | true | Master switch for tool result eviction (read, grep and the listing tools). Defaults to true. |
compaction.result_eviction.keep_recent |
integer or null | 2 | How many most recent evictable read and grep results (read pages, grep dumps) stay intact as a working window. 0 keeps none. Listing results do not count toward it. The default of 2 keeps a read and a grep live at the same time; with 1, a model comparing two results keeps re-fetching whichever the other evicted. |
compaction.result_eviction.min_result_bytes |
integer or null | 2000 | Results at or below this size are never evicted (too small to be worth a placeholder). 0 makes every result a candidate. |
compaction.result_eviction.start_percent |
integer or null | 50 | Evict only once the estimated context reaches this percent of the model's context window. Below it the replayed history is sent untouched, so the provider's prompt cache keeps it; a placeholder appearing mid-history invalidates every cached token behind it. 0 evicts from the first result. Read and grep results of a model without max_context_tokens are evicted from the first result; listing results measure against the window its provider reports, or the default 128000. |
Optional memory subagent (implementation in external/memory; enable at runtime with memory.enable).
| Key | Type | Default | Description |
|---|---|---|---|
memory.enable |
boolean | false | Run the memory subagent on every user turn (needs the memory build tag). |
memory.model |
string | "" | Exact models[].model id the memory subagent runs on; empty uses the session's model. |
memory.fallback_models |
list of strings | Memory subagent models tried in order when the one before them fails before answering (models[].model ids). The session's own model is the last resort whether or not it is listed, so one unreachable deployment does not take the memory run down with it. | |
memory.dir |
string | "" | Long-term memory root. Empty resolves to ${CODDY_HOME}/memory. Supports ${CODDY_HOME} and ~. |
memory.wait_seconds |
integer or null | 20 | How long a user turn waits for the memory subagent's report before its first model call. An explicit 0 never waits: the report then reaches the turn only through a later step, or stays in the Tasks panel. |
memory.timeout_seconds |
integer | 300 | Hard limit of one memory run in seconds, capped by tools.background.max_timeout_seconds like every task of the pool. |
memory.keep_runs |
integer or null | 20 | Finished memory runs kept per session, task record and child transcript alike; the oldest beyond this number are removed when a run finishes. An explicit 0 keeps every run. |
memory.recall_max_turns |
integer | 6 | Bounds the memory subagent's ReAct rounds together with persist_max_turns; the child's cap is the larger of the two. |
memory.persist_max_turns |
integer | 12 | Bounds the memory subagent's ReAct rounds together with recall_max_turns; the child's cap is the larger of the two. |
memory.copilot_max_tokens |
integer | 4096 | Completion token cap for the memory model's calls. |
memory.max_search_hits |
integer | 8 | Maximum snippets returned by memory_search. |
memory.max_note_chars |
integer or null | 900 | Longest body one note saved by coddy_memory_save may have, in characters (not bytes). An explicit 0 removes the cap. |
memory.additional_prompt |
string | "" | Operator instructions for the memory subagent alone: a section of its system prompt that the main agent never sees. Empty adds nothing. |
memory.additional_prompt_max_chars |
integer | 0 | Cap on additional_prompt in characters; a longer text is cut there, the agent log says so and coddy -t reports it. 0 means no cap. |
OpenAI-compatible HTTP API defaults (used only by binaries built with -tags http; the embedded SPA needs -tags http,ui). See https://coddy.dev/docs/reference/http-api.
| Key | Type | Default | Description |
|---|---|---|---|
httpserver.enable |
boolean or null | true | Serve the HTTP API (and the embedded SPA) in this process. Omitted means true; set false on a node that only polls a messenger or relays a swarm. |
httpserver.host |
string | 127.0.0.1 | Bind address for the HTTP API. Empty means 127.0.0.1; set 0.0.0.0 to accept connections from other machines. |
httpserver.port |
integer | 0 | Default listen port when coddy serve does not pass -P/--port. 0 falls back to 12345. |
httpserver.auth_token |
string | "" | Optional bearer credential for the HTTP API. Empty means no authentication (historical default). Use a "${ENV}" reference to keep the secret out of the file; never echoed back by GET /coddy/config. Prefer --auth-token / CODDY_HTTP_TOKEN to keep it out of config.yaml entirely. See https://coddy.dev/docs/operate/remote. |
httpserver.login |
object | Optional password sign-in for the browser, so a server on a network is not readable by everyone who finds the port. Off unless an account exists here or in CODDY_HTTP_USER / CODDY_HTTP_PASSWORD. API clients keep using auth_token. See https://coddy.dev/docs/operate/remote. | |
httpserver.login.enable |
boolean or null | Turn the sign-in form on or off explicitly. Omitted follows the credentials: an account here or in the environment enables it. false keeps the form off with the variables still set. | |
httpserver.login.mode |
string, one of password
|
password | How a browser authenticates. Only "password" is implemented; the key exists so a trusted-proxy mode can be added without moving your configuration. |
httpserver.login.user |
string | "" | Account name for the sign-in form. Supports "${ENV}" references, e.g. "${CODDY_HTTP_USER}". |
httpserver.login.password_hash |
string | "" | argon2id hash of the password in PHC form ("$argon2id$v=19$m=...$..."), written by coddy serve set-password. Never echoed back by GET /coddy/config, and a save from the settings screen preserves it. Put a plaintext password in CODDY_HTTP_PASSWORD instead of here. |
httpserver.login.session_ttl_hours |
integer | 0 | How long a browser stays signed in. 0 means the cookie is dropped when the browser closes; the server still expires the session itself after 30 days, because a record it keeps forever is not a session. |
httpserver.public_docs |
boolean | false | When auth is enabled, keep /docs and /openapi.* reachable without a token. |
httpserver.allow_insecure |
boolean | false | Silence the two startup warnings about a server without authentication - a non-loopback bind, and CORS that admits pages nobody listed (allow_loopback or "*") - and the --dry-run findings that mirror them. |
httpserver.cors |
object | Cross-origin access so a browser UI on another origin can call this API (e.g. the bundled UI pointed at a remote server). Bearer auth still applies: CORS decides whether a browser shows a page the answer, never whether the server gives one. | |
httpserver.cors.enable |
boolean | false | Handle CORS preflight and emit Access-Control-* headers for allowed origins. |
httpserver.cors.allow_loopback |
boolean | false | Also allow any page served from the browser's own machine: an http or https origin whose host is localhost, a .localhost name, 127.0.0.0/8 or [::1], on any port. The laptop case of a remote coddy serve, where the web UI comes from the laptop's own coddy serve and its port moves. Narrower than "", and like "*" only as safe as the token or sign-in behind the API. |
httpserver.cors.allowed_origins |
list of strings | [] | Exact origins permitted to call the API, e.g. "http://localhost:5173". A single "*" allows any origin. |
httpserver.remotes |
list of objects | [] | Remote coddy serve servers and swarm relays offered in the UI environment selector and resolved by coddy --remote <name>. An entry may carry the token to present; without one the UI keeps the token in the browser per remote and the console reads --remote-token or CODDY_REMOTE_TOKEN. |
httpserver.remotes[].name |
string | Display label for the remote. Empty: the web UI shows the name the remote reports (a relay's swarm.name, else the host name it runs on; an agent's host name), else its address. | |
httpserver.remotes[].url |
string | Base URL of the remote coddy serve server or relay, e.g. "https://box.example:12345". | |
httpserver.remotes[].token |
string | Bearer token to present to this remote: a relay's client token (swarm.auth_token) or a server's httpserver.auth_token, usually a ${ENV} reference. Optional and opt-in: every browser that reads this configuration receives it, and coddy --remote uses it when --remote-token is not given. A node mounted under a relay (/swarm/nodes/) takes the relay's token. |
Stateless relay that nodes register into and that chains into other relays. The relay itself needs -tags swarm and swarm.enable; the join list below is honoured by any coddy serve process, which is what makes an ordinary agent reachable through a relay. See https://coddy.dev/docs/operate/swarm.
| Key | Type | Default | Description |
|---|---|---|---|
swarm.enable |
boolean | false | Run the swarm relay in this process. Independent of swarm.join, which is how an agent registers into a parent relay. |
swarm.host |
string | "" | Bind address for the relay when the CLI does not pass --swarm-host. Empty falls back to 0.0.0.0. |
swarm.port |
integer | 0 | Listen port for the relay. 0 falls back to 12346. |
swarm.name |
string | "" | Label for this relay in topology views, in /swarm/info and in a child's node path. Empty: the host name it runs on. |
swarm.auth_token |
string | "" | Bearer credential clients present to this relay. A relay reaches every node with that node's own credential, so an unauthenticated relay bound off loopback is a fleet-wide open door and the server refuses to start unless allow_insecure is set. Never echoed back. |
swarm.pairing_tokens |
list of strings | [] | Credentials a node must present to register. Empty closes registration unless insecure_open_registration is set. Never echoed back. |
swarm.allow_insecure |
boolean | false | Permit binding off loopback without a client token. |
swarm.insecure_open_registration |
boolean | false | Let any caller register a node without a pairing token. Development only. |
swarm.allow_private_upstreams |
list of strings | [] | Hosts a node may advertise even though they resolve into loopback or private ranges, which are otherwise refused so a registration cannot turn the relay into a probe of its own network. |
swarm.cors |
object | Cross-origin access for the relay API. The SPA reaches a relay from another origin by construction, so this usually has to be on. Allow-Headers includes Last-Event-ID so an SSE stream can be resumed through the relay. | |
swarm.cors.enable |
boolean | false | Handle CORS preflight and emit Access-Control-* headers for allowed origins. |
swarm.cors.allow_loopback |
boolean | false | Also allow any page served from the browser's own machine: an http or https origin whose host is localhost, a .localhost name, 127.0.0.0/8 or [::1], on any port. A laptop's web UI reaching this relay, whatever port its own coddy serve took. Narrower than ""; the client token still applies. |
swarm.cors.allowed_origins |
list of strings | [] | Exact origins permitted to call the API, e.g. "http://localhost:5173". A single "*" allows any origin. |
swarm.tls |
object | Serve the relay over HTTPS. Set both files or neither. Minimum TLS 1.2; certificates are startup state, so rotating them needs a restart. | |
swarm.tls.cert_file |
string | "" | PEM certificate chain. |
swarm.tls.key_file |
string | "" | PEM private key. |
swarm.lease_ttl_seconds |
integer | 0 | How long a registration survives without a heartbeat. 0 falls back to 90; nodes refresh at a third of it. |
swarm.fanout_timeout_seconds |
integer | 0 | Per-node deadline for an aggregated call. A slower node degrades into a warning rather than stalling the answer. 0 falls back to 3. |
swarm.upstreams |
list of objects | [] | Nodes configured by hand rather than registered, for an agent that cannot run the join loop. |
swarm.upstreams[].name |
string | Node name; becomes a URL path segment, so letters, digits, underscore and hyphen only. | |
swarm.upstreams[].url |
string | Origin the relay dials to reach the node. | |
swarm.upstreams[].kind |
string, one of "", agent, relay |
"" | agent (default) or relay. |
swarm.upstreams[].token |
string | "" | Bearer credential the relay presents to this node. Never echoed back. |
swarm.upstreams[].dial |
object | How this leg reaches the other side. Relays are expected to live in different networks, so a proxy hop and a private certificate authority are ordinary here. | |
swarm.upstreams[].dial.proxy |
string | "" | Route the connection through a proxy: http, https, socks5 or socks5h. Empty falls back to the standard environment variables. Never echoed back. |
swarm.upstreams[].dial.ca_file |
string | "" | Certificate authority used to verify a peer whose certificate is signed privately. |
swarm.upstreams[].dial.insecure_skip_verify |
boolean | false | Accept any certificate. For a lab only; every use is logged. |
swarm.join |
list of objects | [] | Parent relays this process registers into on startup. Honoured whether or not this process runs a relay of its own, which is what lets relays chain. |
swarm.join[].url |
string | Parent relay origin. | |
swarm.join[].name |
string | "" | Name to claim in that relay. Empty falls back to the host name. |
swarm.join[].pairing_token |
string | "" | Credential authorising the registration. Never echoed back. |
swarm.join[].advertise_url |
string | "" | Where the relay can reach this process. Leave empty to select the tunnel transport: this process dials out and serves its API back over that connection, which is the only way in when the network accepts no inbound connections. |
swarm.join[].token |
string | "" | This process's own bearer credential, handed to the relay so it can authenticate when it proxies. Prefer a credential minted for the relay alone. Never echoed back. |
swarm.join[].labels |
map of strings | Free-form tags shown in topology views. | |
swarm.join[].dial |
object | How this leg reaches the other side. Relays are expected to live in different networks, so a proxy hop and a private certificate authority are ordinary here. | |
swarm.join[].dial.proxy |
string | "" | Route the connection through a proxy: http, https, socks5 or socks5h. Empty falls back to the standard environment variables. Never echoed back. |
swarm.join[].dial.ca_file |
string | "" | Certificate authority used to verify a peer whose certificate is signed privately. |
swarm.join[].dial.insecure_skip_verify |
boolean | false | Accept any certificate. For a lab only; every use is logged. |
Controls the bundled single-page UI (only meaningful in binaries built with -tags http,ui).
| Key | Type | Default | Description |
|---|---|---|---|
ui.enable |
boolean or null | true | Serve the embedded SPA at GET /. Defaults to true; set false to run an API-only server (the API still requires httpserver.auth_token when configured). |
Cron-driven scheduled jobs (used only by binaries built with -tags scheduler). Jobs are flat *.md files with YAML frontmatter in ${CODDY_HOME}/scheduler (user jobs) and /.coddy/scheduler (project jobs, which run once approved); five-field crontab in UTC. A run is a background agent task under the job's own session, which is the job's run history.
| Key | Type | Default | Description |
|---|---|---|---|
scheduler.enable |
boolean | false | Run the scheduler daemon and expose the coddy_scheduler_* tools. Can also be forced per process with coddy serve --scheduler (or coddy acp -scheduler). |
scheduler.max_queue |
integer | 10 | Runs in flight across all jobs at once; a due slot past the cap is skipped until a run finishes, and a manual run past it is refused. |
scheduler.timeout |
string | 30m | Wall-clock limit for one run, as a Go duration (e.g. "30m", "1h30m"); the background task pool still caps it at tools.background.max_timeout_seconds. |
scheduler.project_trust |
string, one of ask, allow, deny |
ask | Trust policy for project jobs, the *.md files in /.coddy/scheduler that travel with the checkout: "ask" lists them but runs one only once the operator approved that exact file for that workspace (the shield in the scheduler drawer, or POST /coddy/scheduler/jobs/{job_id}/trust?scope=project from a session in it); a job the operator creates through Coddy is approved at once; "allow" runs them like the operator's own jobs; "deny" never runs them. See https://coddy.dev/docs/operate/scheduler#project-jobs-and-trust. |
scheduler.retain_sessions |
integer | 5 | Finished runs kept per job_id (their task records and transcripts under the job session); older runs are removed when a run finishes. |
Messenger bot adapters (used only by binaries built with -tags gateway, or -tags gateway.telegram / gateway.pachca for one adapter; started by coddy serve alongside every other enabled subsystem). See https://coddy.dev/docs/surfaces/gateway and https://coddy.dev/docs/surfaces/pachca.
| Key | Type | Default | Description |
|---|---|---|---|
gateways.telegram |
object | Telegram bot adapter. | |
gateways.telegram.enable |
boolean | false | Poll Telegram in this coddy serve process. |
gateways.telegram.token |
string | "" | Bot token from @BotFather. Leave empty to read the TELEGRAM_BOT_TOKEN environment variable (e.g. via ~/.coddy/.env). |
gateways.telegram.proxy |
string | inherit | inherit (the default, the same as leaving the key out), none, or a proxy URL: http://, https://, socks5:// or socks5h://, read like providers[].proxy. inherit follows the proxy the environment of the Coddy process names - HTTPS_PROXY, HTTP_PROXY and NO_PROXY, never for a loopback address; ALL_PROXY is not read. none connects the bot directly and ignores those variables. A proxy URL sends every Bot API request through that proxy; with SOCKS the proxy resolves host names. |
gateways.telegram.rich_messages |
boolean | false | Use Bot API 10.1 Rich Messages (native Markdown, streamed thinking placeholder, collapsible tool list). Falls back to legacy formatting when unsupported. |
gateways.telegram.admins |
list of integers | Telegram user IDs with elevated rights; admins always pass access checks. | |
gateways.telegram.default_access |
string | all | Fallback access level for chats without an override: "all", "admins", or "group:". |
gateways.telegram.default_isolation |
string, one of individual, shared, admin |
individual | Fallback session isolation for group chats: "individual" (session per user), "shared" (one session for all), "admin" (admins only, shared session). |
gateways.telegram.user_groups |
list of objects | Named sets of user IDs referenced from access fields as group:. | |
gateways.telegram.user_groups[].name |
string | Group name referenced as group:. | |
gateways.telegram.user_groups[].user_ids |
list of integers | Telegram numeric user IDs in this group. | |
gateways.telegram.chats |
list of objects | Per-chat overrides; chat_id is negative for groups and supergroups. | |
gateways.telegram.chats[].chat_id |
integer | Telegram chat id (negative for groups/supergroups). | |
gateways.telegram.chats[].isolation |
string, one of individual, shared, admin |
Per-chat session isolation override. | |
gateways.telegram.chats[].access |
string | Per-chat access override: "all", "admins", or "group:". | |
gateways.telegram.mini_app |
object | Makes the web UI of this coddy serve the bot's Telegram Mini App. The web UI adapts to Telegram by itself; these keys tell the bot where it is, so it can set its menu button and answer /app with a button that opens the chat's own conversation. Telegram opens Mini Apps over https only, so the web UI has to be published behind a TLS proxy, with sign-in on (httpserver.login). See https://coddy.dev/docs/surfaces/gateway#mini-app. | |
gateways.telegram.mini_app.url |
string | "" | The public https address the web UI is served at, for example https://coddy.example.com/. Plain http is accepted only for a loopback host (an offline stand such as tgfake). No fragment: Telegram puts its launch parameters there. Empty: the bot offers no Mini App and leaves a menu button set in @BotFather alone. |
gateways.telegram.mini_app.menu_button |
boolean or null | true | Make the bot's menu button (beside the message field) open the web UI. Turned off, with url emptied or while the web UI asks for no sign-in, the bot puts back the menu button it replaced (a Mini App set in @BotFather, else the commands), as long as the button still opens the address the bot set. |
gateways.pachca |
object | Pachca integration bot adapter: reads the bot's events history and answers through the Pachca REST API. Turn on Save events history (events_history_enabled) in the bot's outgoing webhook settings. See https://coddy.dev/docs/surfaces/pachca. | |
gateways.pachca.enable |
boolean | false | Run the Pachca bot in this coddy serve process. |
gateways.pachca.token |
string | "" | Access token of the Pachca integration bot. Leave empty to read the PACHCA_BOT_TOKEN environment variable (e.g. via ~/.coddy/.env). |
gateways.pachca.proxy |
string | inherit | inherit (the default, the same as leaving the key out), none, or a proxy URL: http://, https://, socks5:// or socks5h://, read like providers[].proxy. inherit follows HTTPS_PROXY, HTTP_PROXY and NO_PROXY of the Coddy process; none connects directly; a proxy URL sends every Pachca API request through that proxy. |
gateways.pachca.poll_interval_seconds |
integer | 2 | How often the events history is read, in seconds (1 to 60). Pachca answers that method at about five requests per two seconds. |
gateways.pachca.admins |
list of integers | Pachca user IDs with elevated rights; admins always pass access checks. | |
gateways.pachca.default_access |
string | all | Fallback access level for chats without an override: "all", "admins", or "group:". |
gateways.pachca.default_isolation |
string, one of individual, shared, admin |
individual | Fallback session isolation for group chats: "individual" (session per user), "shared" (one session for all), "admin" (admins only, shared session). |
gateways.pachca.user_groups |
list of objects | Named sets of user IDs referenced from access fields as group:. | |
gateways.pachca.user_groups[].name |
string | Group name referenced as group:. | |
gateways.pachca.user_groups[].user_ids |
list of integers | Pachca user IDs in this group. | |
gateways.pachca.chats |
list of objects | Per-chat overrides keyed by the Pachca chat id (a thread has a chat id of its own). | |
gateways.pachca.chats[].chat_id |
integer | Pachca chat id. | |
gateways.pachca.chats[].isolation |
string, one of individual, shared, admin |
Per-chat session isolation override. | |
gateways.pachca.chats[].access |
string | Per-chat access override: "all", "admins", or "group:". |
Ask the NeuralDeep decisions API whether a shell command is safe before the permission gate decides: a safe command runs without a prompt, an unsafe one is asked about in every mode, bypass included, and rejected where nobody can answer. See https://coddy.dev/docs/features/decisions.
| Key | Type | Default | Description |
|---|---|---|---|
decisions.enable |
boolean | false | Check run_command and ssh_run_command calls the operator did not allow explicitly against the decisions endpoint before the permission gate decides. A safe command runs without a prompt; an unsafe one, or one the check gave no verdict on, is asked about in every mode, bypass included, with the verdict in a prompt no mode or surface answers by itself, and where nobody can answer it is rejected, the rejection returned as the tool result so the session records it. Commands the allowlist, a session grant of exactly that command or a hook's allow approves are not checked, and neither are commands the operator approved in a prompt; a program-wide or prefix grant does not exempt a command from the check. Needs a NeuralDeep credential on the first provider row of type neuraldeep: its api_key or api_key_command, its NAME_API_KEY environment variable (NEURALDEEP_API_KEY for a row named neuraldeep or when there is no row) or its stored hub sign-in. |
decisions.model |
string, one of frida-decisions, clef-flash |
frida-decisions | Decisions model to ask: frida-decisions (encoder pass, up to 512 tokens of command text, ~20 ms per request) or clef-flash (up to 8192 tokens, ~150 ms). |
decisions.threshold |
number | 0.5 | Probability of the unsafe option at or above which a command counts as unsafe. 0 uses the default (0.5); lower is stricter, higher lets borderline commands through. A bare unsafe answer counts as 1. |
Prometheus exporter: GET /metrics on a port of its own, run by coddy serve. It takes effect only in a binary built with -tags metrics; other binaries refuse metrics.enable. Off by default.
| Key | Type | Default | Description |
|---|---|---|---|
metrics.enable |
boolean | false | Run the exporter in this process. Needs the metrics build tag. |
metrics.host |
string | "" | Bind address of the exporter. Empty falls back to 127.0.0.1, which only this machine can reach. |
metrics.port |
integer | 0 | Listen port of the exporter. 0 falls back to 12347. |
metrics.token |
string | "" | Bearer credential a scrape must present in its Authorization header. ${ENV} references are expanded at load. Empty means no authentication, so set one before binding off loopback. Never echoed back. |
Checks a session goal (/goal) after every turn with a second model, continues unfinished work within bounds, and watches for stalled or looping turns; see https://coddy.dev/docs/features/session-supervisor.
| Key | Type | Default | Description |
|---|---|---|---|
supervisor.enable |
boolean | false | Check ordinary turns against the latest request even without a /goal. A session goal is checked whatever this switch says. |
supervisor.model |
string | "" | Configured models[].model that checks the work. Empty uses the session's own model, which then grades its own work; a model of another family is the better judge. |
supervisor.verify |
boolean or null | true | Confirm a met verdict before the goal is closed: the goal-verifier subagent reads the workspace with read-only tools and checks every requirement. Unset means true. |
supervisor.stall_seconds |
integer or null | 300 | Seconds a goal turn may go without any update before the watchdog cuts it and starts a recovery turn. Permission prompts, running tools and running background tasks pause the timer. 0 disables it. |
supervisor.max_nudges |
integer or null | 2 | Recovery turns after a stall, a tool loop or a failed turn, per run of the supervisor. |
supervisor.max_continuations |
integer or null | 10 | Automatic continuation turns one goal may use; when they run out the goal gets one wrap-up turn and stops as limited. |
supervisor.loop_repeat |
integer or null | 3 | How many times a repeating cycle of identical tool operations (same call, same result) may come round before the watchdog cuts the turn. 0 disables loop detection. |
supervisor.token_budget |
integer or null | 0 | Uncached input plus output tokens one goal's turns may spend; 0 means no cap. |
supervisor.max_chain_sessions |
integer or null | 10 | Sessions one goal chain (/goal --then-file) may open one after another; when the goal of the last one is met, the chain stops. 0 turns goal chains off. |
Named presets of tool lists. A models[] row names one with toolset, and a session can pick one with /toolset , on its own or after /model on the same line. A preset only narrows: a tool the mode, a subagent definition or a switch already hides stays hidden, and a call to a tool outside the set is refused without a permission prompt. It is a context optimization, not a security boundary.
| Key | Type | Default | Description |
|---|---|---|---|
toolsets |
list of objects | Named presets of tool lists. A models[] row names one with toolset, and a session can pick one with /toolset , on its own or after /model on the same line. A preset only narrows: a tool the mode, a subagent definition or a switch already hides stays hidden, and a call to a tool outside the set is refused without a permission prompt. It is a context optimization, not a security boundary. | |
toolsets[].name |
string | Name of the preset: letters, digits, '.', '_' and '-'. default is reserved: a session picks it to be offered every tool the mode offers. | |
toolsets[].tools |
list of strings | Allowlist: tool names, a bare , or a prefix (context7__* admits every tool of that MCP server). Absent or empty admits every tool the mode would. coddy -t warns about an entry that matches no tool. | |
toolsets[].disallowed_tools |
list of strings | Denylist applied after tools, with the same names and patterns; a name in both lists is removed. coddy -t warns about an entry that matches no tool. |
List of LLM backends ([]config.ProviderConfig, internal/config/providers.go).
Key resolution order: api_key → api_key_command stdout → NAME_API_KEY env var. A codex row signs in with ChatGPT and reads none of them.
providers:
- name: openai
type: openai
api_key: "${OPENAI_API_KEY}"
- name: local
type: openai
api_base: "http://localhost:11434/v1"
api_key: "~"
- name: neuraldeep
type: neuraldeep
api_key: "${NEURALDEEP_API_KEY}"
# usage_limits_panel: false # hide the account usage panel for this row
- name: codex
type: codex # use Sign In with ChatGPT in the bundled web UI; no api_key neededllama-server works as a type: openai provider (api_base: "http://host:8080/v1"). Recommended launch flags:
--jinja- enables the model's chat template on the server, which is required for tool calling. Without it llama.cpp silently ignores thetoolsparameter and the agent loop degrades to plain text answers.-c <n>- set the context window large enough for an agent prompt (system prompt plus tool schemas plus history; 16k is a practical minimum, more is better). When a request exceeds the server context, llama.cpp reportsthe request exceeds the available context size- raise-cor trimmax_context_tokens.
llama.cpp builds through 2025 report mid-stream failures with a non-standard SSE error: field; Coddy understands both that dialect and the current data: {"error": ...} shape and surfaces the server's message in the error.
For type: codex, open Settings → LLM Providers in the bundled web UI and select Sign In with ChatGPT, or run the terminal equivalent for ACP and headless setups:
coddy providers login codex # device flow: prints a URL and a one-time code, then waits
coddy providers list # every provider with the credential its requests actually use,
# plus a warning for an openai row that has neither api_base
# nor a credential (its requests leave for api.openai.com)
coddy providers logout codex # removes the Coddy-managed credential (leaves the Codex CLI login alone)(Up to 1.0.11 the same flow also had a codex-only command, coddy codex login | status | logout. It was removed in favour of the commands above, which cover every backend; coddy providers list reports what codex status used to.)
A successful login also publishes the subscription catalog into config.yaml, the way coddy providers login neuraldeep publishes its tier models (the Settings button stores the credential only): it adds the codex provider row when it is missing, one models[] entry per catalog model Codex lists (the ids Codex hides from its own picker, such as gpt-reserve and codex-auto-review, are left out), and agent.model when nothing is set yet - Codex's own top-ranked model. Nothing already in the file is rewritten: an existing provider row, an already listed model, and a chosen agent.model survive untouched, so a repeated login is a no-op. --no-config stores only the credential. Entries carry no max_tokens, because the Codex backend rejects max_output_tokens. A running coddy serve keeps its loaded config, so restart it to pick the new models up.
Both paths use the same storage; the provider name is the argument, so coddy providers login <name> targets a specific codex row when config.yaml defines several (a row under a name other than codex either exists in config.yaml or is created by the login with --type codex). Each codex row keeps a login of its own; the Codex CLI login stands in only for the only codex row, or the row named codex among several (Several profiles of one provider type). Coddy uses the official device authorization flow and stores refreshable credentials at $CODDY_HOME/providers/<provider-name>/codex-auth.json with restrictive file permissions; tokens never enter config.yaml. api_key, api_key_command, and api_base are ignored for Codex (an api_key_command left on a codex row never runs), while proxy applies to OAuth and provider requests. A Codex model has no output cap Coddy can set: the backend answers Unsupported parameter to max_output_tokens, so models[].max_tokens on a codex model bounds nothing. It is still accepted - the settings form seeds it on every model row - but coddy -t, --dry-run and the startup log of coddy serve warn about it, and a max_tokens sent on POST /v1/chat/completions for a codex model is refused with 400. max_context_tokens is a different thing: the window the context ring and automatic compaction measure against, not a limit the provider enforces. Left unset on a codex model, it is the window the Codex catalog reports for the model, context_window (272000 for every model it serves today) (The context window). The model picker reads the catalog from the official Codex backend with the saved token. The backend offers each model only to Codex releases from the model's minimal_client_version on, since an older Codex binary cannot run it; Coddy runs every model through its own loop and tools, so it asks as Codex release 0.999.0, far above the releases the gates name, and lists every model the account is offered, the newest included, whichever Codex CLI is installed, if any. The models Codex hides from its own picker stay out of the list. If no Coddy-managed credential exists, Coddy remains compatible with a Codex CLI login in ~/.codex/auth.json (or $CODEX_HOME/auth.json). Codex requests always target the official backend; the process-level CODDY_CODEX_BASE_URL is the only override (tests and self-hosted gateways), so a settings document can never redirect an OAuth token on its own.
Codex is only a model backend: the agent keeps Coddy's own system prompt, tool catalog, permissions, and ReAct loop, and the ChatGPT credential is used solely to authenticate the Responses calls (features/codex_auth.feature pins this on both the HTTP and ACP surfaces).
Token lifetime. The access token is refreshed transparently shortly before it expires, and the refreshed tokens are written back to the file they came from. When the credential is the Codex CLI login (for the one row that login serves), that file is ~/.codex/auth.json itself - the same file the codex CLI reads, so both tools keep working off one login, and a refresh performed by Coddy is visible to the CLI (and vice versa). A Coddy-managed credential is refreshed in place under $CODDY_HOME/providers/<name>/ and never touches the CLI login. Signing out (coddy providers logout codex, or Sign Out in Settings) removes only the Coddy-managed file.
Startup report. When at least one type: codex provider is configured, coddy acp and coddy serve log one codex credential line per provider at startup: where the credential came from and how long the access token is still valid. A missing credential, an unusable auth_mode, or an expired token with no refresh token left is logged as a warning naming coddy providers login <name>; an expired but refreshable token is only an informational line, since the next request renews it. Setups without a codex provider log nothing.
Reasoning. The Codex backend serves gpt-5* and gpt-6* model ids but accepts only none, low, medium, high, and xhigh, so codex-backed models offer none where other providers offer minimal (an explicit reasoning_levels: [minimal] is remapped as well). Reasoning turns request summaries (summary: auto) so thinking streams into the UI, and encrypted reasoning (include: reasoning.encrypted_content) so the model's own chain of thought is replayed verbatim on the next request of the same turn - the same flow the Codex CLI uses. Replayed reasoning is tagged with the model that produced it and is skipped when the session switches models. The items are stored opaquely in messages.json (reasoning_signature, ~1 KB per assistant turn) and are not exposed by GET /coddy/sessions/{id}/messages.
List of logical models ([]config.ModelEntry, internal/config/models.go).
models:
- model: "openai/gpt-5.6-terra"
max_tokens: 8192
reasoning_default: medium
multimodal: true
- model: "openai/gpt-5"
max_tokens: 8192
reasoning_default: medium
- model: "local/qwen3-30b"
max_tokens: 8192
stream: falseNon-streaming models. stream: false changes one thing: coddy sends a single blocking POST /chat/completions instead of asking for SSE, and hands the finished answer to the rest of the runtime in one piece. Everything downstream is unchanged, so the transcript, tool calls, and session bundle look the same; what differs is that nothing appears until the model is done, and the thinking row shows no live duration. Two consequences are worth knowing before turning it on. Pressing Stop during a blocking call loses the whole answer, because the server has sent nothing yet, whereas a streamed turn keeps the tokens that already arrived. And a client asking for an SSE stream still gets one - the switch governs the connection to the LLM, not the connection to the client - which is why a streaming HTTP response now carries a keepalive comment every 15 s so proxies do not drop a turn that stays silent for minutes.
The switch is rejected for type: codex providers. The Codex Responses backend has no blocking mode, so honoring the key there would mean streaming anyway and only pretending not to.
ReAct loop settings (config.Agent, internal/config/agent.go).
System prompt template overrides (config.Prompts, internal/config/prompts.go). Template fields are documented in Configuration.
Instruction files the operator adds to the prompt (config.Instructions, internal/config/instructions.go). The list is empty by default and only adds: the AGENTS.md and DESIGN.md of the agent home, of the session folder and of the folders a tool enters are read whether or not it names them, and its files come after them, in the order listed. An entry naming a file the prompt already carries, such as the session folder's own AGENTS.md, is skipped. See rules.md.
Skill discovery (config.Skills, internal/config/skills.go).
Rules discovery (config.Rules, internal/config/rules.go), from the workspace folders and from the operator's own ${CODDY_HOME}/rules. See rules.md.
MCP settings that are not tied to a single server entry (config.MCP, internal/config/mcp.go).
The servers themselves are not declared in config.yaml. They live in two Cursor-compatible
files, a single mcpServers object keyed by server name (env and headers are JSON
objects, per-tool switches use disabledTools): the user-global ${CODDY_HOME}/mcp.json
(~/.coddy/mcp.json by default, like Cursor's ~/.cursor/mcp.json, the "global" scope) and
the project-local <workspace>/.coddy/mcp.json (the "local" scope), which wins a name over
the global file and whose entries need a workspace approval before they are started. An old
mcp_servers list in config.yaml is moved into ${CODDY_HOME}/mcp.json on the next load,
with a config.yaml.bak-<time> copy of the file kept, and coddy -t reports it as a warning.
See mcp.md.
Added for issue #80.
Approvals are recorded in ~/.coddy/mcp-trust.json, keyed by the canonical workspace path
(a linked git worktree uses its main checkout's) and a digest of the command-bearing declaration (transport, command, args, env, url,
headers), so rewriting an approved entry asks again. Approve with coddy mcp trust <name>,
POST /coddy/mcp/{name}/trust, or the shield button in Settings → MCP servers. That tab
also edits this policy itself (POST /coddy/mcp/project-trust), so it is not rendered as a
separate settings section. See docs/features/mcp.md.
Permission policy (config.Tools, internal/config/tools.go).
Maximum lines a tool result or error may return into the LLM context (config.ToolOutputLimits). Every enabled line limit also applies a hard 64 KiB per-call byte ceiling, preventing a minified file, base64 payload, or one-line MCP response from bypassing the guard. 0 disables both limits for that tool; an unset field falls back to the built-in default. Truncated output ends with a marker telling the model how to fetch the rest (offset/limit for read, a narrower pattern for grep, page for websearch).
Bounds for background execution (config.ToolBackground). A backgrounded run_command returns a task id instead of output; background_list, background_output, background_wait, and background_stop collect the result later. The pool lives inside the running coddy process: each task mirrors its metadata and captured output into the session bundle under background/<task_id>/, and a task interrupted by a restart is reported as orphaned rather than as still running. 0 on any integer field means "use the default". See docs/features/background-tasks.md.
Subagents (config.Subagents, internal/config/subagents.go): child agents the model delegates to with the spawn_agent tool. A definition is a markdown file with YAML frontmatter (name, description, model, reasoning, mode, tools, disallowed_tools, permission_mode, max_turns, timeout_seconds, background, hidden) whose body is the child's role. Each run is a background task of the parent session with its own child session and transcript, so background_list / background_output / background_wait / background_stop, the Tasks panel and GET /coddy/sessions/{id}/background-tasks all see it. 0 on max_concurrent, default_timeout_seconds and max_turns means "use the default"; max_depth is the exception, omit it for the default 1, because an explicit 0 forbids spawning everywhere. See docs/features/subagents.md.
Approvals for project-scope definitions are recorded in ~/.coddy/subagents-trust.json, keyed by the canonical workspace path (a linked git worktree uses its main checkout's), the definition name and a digest of the file, so editing an approved file asks again. permission_mode, tools and disallowed_tools in a definition can only narrow what the parent could do, in every scope.
Hooks (config.Hooks, internal/config/hooks.go): operator commands run at lifecycle points of a session. A hook reads one JSON document on stdin and answers with an exit code plus optional JSON on stdout; a PreToolUse hook can deny a tool call whatever the permission mode, approve it past the prompt, force the prompt, rewrite its arguments or add context, and a PostToolUse / PostToolUseFailure hook can add feedback. Definitions are JSON files in Claude Code's shape. 0 on every integer key means "use the default". See docs/features/hooks.md.
Approvals for project-scope files are recorded in ~/.coddy/hooks-trust.json, keyed by the canonical workspace path (a linked git worktree uses its main checkout's), the workspace-relative file path and a digest of the file, so editing an approved file asks again.
Logging (config.Logger, internal/config/logger.go). ACP flags --log-level, --log-output, --log-file, --log-format override these when set.
levels raises or lowers verbosity one subsystem at a time, so chasing a Telegram command that does not work no longer means turning the whole process to debug and reading it out of everything else:
logger:
level: "info"
levels:
- component: "gateway.telegram"
level: "debug"A component is the dotted name a subsystem tags its logger with, and it stays on every record that subsystem writes, so a file can also be filtered by subsystem after the fact. A parent name covers everything nested under it and the longest configured prefix wins: gateway reaches gateway.telegram unless that name carries its own entry. A record from an untagged part of the process has no component and follows level. Configuring one component twice is an error rather than a silent winner.
--log-level accepts the same spec as a comma-separated list, which is how an operator running under systemd raises one subsystem for a single restart without editing the file:
coddy serve --log-level "info,gateway.telegram=debug"A bare --log-level debug sets only the root level and leaves the configured entries alone; a spec that names components replaces them, so the flag is a complete statement of what to log.
Session bundle storage (config.Sessions, internal/config/sessions.go).
Context compaction (config.Compaction, internal/config/compaction.go): summarizing older conversation history so long sessions keep fitting the model context window. Applies to the manual compact command and the automatic threshold trigger.
Collapses unmarked read/grep tool results to short placeholders when building the LLM request (config.ResultEviction), so paging a large file or running a wide search cannot pin dead lines in every later turn. A result survives when the model marks it (the keep_result tool, or keep: true on the call) or when it is inside the most recent working window; a filesystem write to a file invalidates earlier reads/greps that covered it. The persisted session bundle keeps every result in full.
The long-term memory subagent (config.MemoryConfig, internal/config/memory.go; implementation in external/memory, memory build tag): the child run every user turn starts in the task pool, the wait for its report, the retention of finished runs (Long-term memory).
OpenAI-compatible HTTP API defaults (config.HTTPServerConfig, internal/config/http.go; http build tag). See http-api.md.
Stateless relay that nodes register into and that chains into other relays (config.SwarmConfig, internal/config/swarm.go; swarm build tag for the server side, though swarm.join is honoured by every coddy serve process). See swarm.md.
Embedded web UI (config.UIConfig, internal/config/ui.go; only meaningful with -tags http,ui).
Cron scheduler (config.SchedulerConfig, internal/config/scheduler.go; scheduler build tag). Job file format is described in Configuration.
Messenger gateways (config.GatewayConfig, internal/config/gateway.go; gateway or gateway.telegram build tag; run with coddy serve). See gateway.md.
These control config discovery itself, not individual fields (see Configuration):
| Variable | Flag equivalent | Meaning |
|---|---|---|
CODDY_HOME |
--home |
Agent state directory (default ~/.coddy). |
CODDY_CWD |
--cwd |
Default session working directory. |
CODDY_CONFIG |
--config |
Explicit path to config.yaml. |
CODDY_SWARM_TOKEN |
--swarm-auth-token |
Client credential of the relay coddy serve runs (see swarm). |
CODDY_SWARM_PAIRING_TOKEN |
--swarm-pairing-token |
Registration credential of that relay (see swarm). |
NAME_API_KEY |
- | Per-provider API key fallback (see providers). |
TELEGRAM_BOT_TOKEN |
- | Telegram bot token fallback (see gateways.telegram). |