This page is the narrative guide. Two companion artifacts cover the full key list:
- config-reference.md - field-by-field tables: type, default, env-var fallback, required/optional, examples.
- config.schema.json - JSON Schema (draft-07) for editor autocomplete and validation, published at https://coddy.dev/config.schema.json so an editor resolves it without a checkout, and embedded into the binary so
coddy -tchecks a file against the same document (see Checking the file from the command line). Any editor with a YAML language server (VS Code YAML extension, Zed, Neovim, Helix) validates keys and values as you type once the file carries this header line:
# yaml-language-server: $schema=https://coddy.dev/config.schema.jsonCoddy writes that line itself. Every save that rewrites config.yaml - the settings screen (PUT /coddy/config), a skill source, the agent's own config_set / config_commit - adds the header when the file has none, and leaves a $schema you chose yourself (a pinned tag, a local path) alone. The same saves keep your comments, including commented-out keys, the order the keys are already in and the way you wrote every value they do not change (see Environment variable references). A save writes the keys the file already has plus whatever actually differs from the built-in defaults - optional fields that were never set are left out entirely rather than written as null, so a file that keeps whole sections commented out stays that way, and a provider or a model entry keeps only the fields it named. JetBrains IDEs do not read the header; if config.yaml is not validated there, map the same URL by hand under Settings - Languages & Frameworks - Schemas and DTDs - JSON Schema Mappings. VS Code can be told the same thing without touching the file:
"yaml.schemas": { "https://coddy.dev/config.schema.json": ["**/.coddy/config.yaml"] }The schema is kept in sync with the Go config structs by TestDocsConfigSchemaMatchesStructs (internal/config/docs_schema_test.go); CI fails when a config field is added or renamed without updating the schema. The copy served from coddy.dev is a verbatim mirror of this file, kept in the site repository.
Resolved locations use environment variables and flags (see README). In short:
CODDY_HOME- agent state directory. Default~/.coddy. Holdsconfig.yaml,sessions/,skills/, Coddy-managed provider credentials underproviders/, andscheduler/when using the optional cron scheduler.CODDY_CWD- default filesystem cwd whensession/newsends an emptycwd. Default is the process working directory at startup. Same meaning as the--cwdflag when set.CODDY_CONFIG- explicit path toconfig.yaml. Same as--config.CODEX_HOME- Codex CLI state directory read by atype: codexprovider when no Coddy-managed credential exists: the only codex row, or the row namedcodexamong several (Several profiles of one provider type). Default~/.codex.CODDY_CODEX_BASE_URL- override for the Codex backend endpoint (defaulthttps://chatgpt.com/backend-api/codex). Process-level on purpose:api_basestays ignored fortype: codex, so a settings document cannot redirect a ChatGPT OAuth token. Used by the executable specs and by self-hosted Codex gateways.CODDY_DEVIN_CLI_CREDENTIALS- the Devin CLIcredentials.tomlread bytype: devinproviders when no Coddy-managed login exists. Default~/.local/share/devin/credentials.toml(or under$XDG_DATA_HOME).CODDY_DEVIN_API_SERVER_URL,CODDY_DEVIN_WEBAPP_URLandCODDY_DEVIN_API_URLmove the Devin endpoints for the whole process, for stands and tests; see Devin.
If no --config is given, the loader uses $CODDY_HOME/config.yaml (default home ~/.coddy). If that file is missing, it tries config.yaml in the process current working directory ($CWD at startup). If neither file exists, built-in defaults apply (no error).
When the primary file exists but is invalid (YAML parse or validation error), the loader automatically recovers from config.yaml.bak in the same directory (see internal/config/recovery.go). After every successful load the server writes config.yaml.bak. The HTTP PUT /coddy/config route (see docs/reference/http-api.md) also snapshots the current file to config.yaml.bak before overwriting, so a failed reload can be rolled back.
The coddy acp subcommand also accepts --home (override CODDY_HOME), --sessions-dir, and --session-id. Optional sessions.dir in the YAML overrides the sessions root when --sessions-dir is not set (default $CODDY_HOME/sessions).
Every command that loads config.yaml also takes -t (long form --test-config): bare coddy -t, coddy cli -t, coddy acp -t and coddy serve -t. The flag checks the file that command would load - --config PATH and --home DIR pick it exactly as they do for a start, ~/.coddy/.env is loaded and ${VAR} references are expanded first - and exits without starting anything. It never writes: the recovery from config.yaml.bak that a normal load performs on a broken file (see above) does not run, so the file you are told about is the file on disk. The flag works in every build, the lean one without the cli tag included.
The check has two stages. First the document is validated against the JSON Schema above, the same one editors use, embedded into the binary. This is what catches the mistakes the loader accepts silently: config.yaml is decoded leniently, so an unknown or misspelled key (enabled for enable) is ignored rather than rejected, and a value of the wrong shape, a value outside an enum or a range, a missing required key or a duplicate key surfaces later as odd behaviour. Then the loader's own rules run on the parsed document: a model naming a provider that does not exist, a file output without logger.file, agent.model missing from models. Every problem is printed as file:line:column: what is wrong, with an indented fix: line saying how to correct it and, where the schema has one, a doc: line carrying the field's description:
$ coddy serve -t
/home/me/.coddy/config.yaml:13:3: httpserver.enabled: unknown key "enabled" (the loader ignores it, so it has no effect)
fix: did you mean "enable"? keys allowed under httpserver: allow_insecure, auth_token, cors, enable, host, port, public_docs, remotes
doc: 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.
/home/me/.coddy/config.yaml:16:10: logger.level: "verbose" is not an allowed value
fix: use one of debug, info, warn, warning, error
doc: Minimum severity written to the configured outputs ("warning" is accepted as an alias of "warn").
/home/me/.coddy/config.yaml:18:11: warning: subagents.enable: "yes" is read as the boolean true, but the schema and editors expect true or false
fix: write enable: true (true or false, unquoted)
/home/me/.coddy/config.yaml: 2 errors, 1 warning
config test failed
The exit status is 1 when the file has errors and 0 otherwise, so the flag fits a deploy script right before coddy serve restart. Warnings (marked warning:) never fail the check: they flag spellings the loader still reads but the schema and editors reject - yes for a boolean, 40.0 for an integer - a file without the # yaml-language-server: header, and a setting the provider never sends: max_tokens on a model served by a codex provider bounds nothing, because the Codex backend takes no output cap. The loader keeps accepting that one, since the settings form seeds max_tokens on every model row it adds, and coddy serve names it in a warning at startup. A missing file is an error, since the flag exists to check the file a start would use. Values under secret-shaped keys (api_key, auth_token, pairing_tokens) are never echoed in a message.
A file that does not parse at all is placed differently from one whose values are merely wrong. The parser reports the line the block it was reading began on, which in a file with a header of comments is a blank line far above the mistake, so the check re-reads the file to find the line whose arrival stops it parsing and reports that one instead. A start prints the same line, so coddy -t and coddy serve send you to the same place.
What an editor leaves in the file is not part of the configuration. A file written on Windows ends its lines with a carriage return and a line feed and may carry a byte order mark in front of the first one; both are dropped on the way in, so the # yaml-language-server: header behind a mark is still found and a finding still names the line the editor shows, and a save puts the file's own line endings back. The one shape Coddy does not read is UTF-16 - Notepad's "Unicode" - which is reported as such, with UTF-8 as the fix, instead of as a syntax error.
--dry-run looks at the world the file describes, after the same check --test-config performs. Every command that takes -t takes it too: coddy --dry-run, coddy cli --dry-run, coddy acp --dry-run, coddy serve --dry-run, with --config and --home selecting the file as for a start. The static check runs first, and a file with errors stops there - probing what a broken file names would only bury the first mistake under its consequences. When the file is clean, the configuration is loaded without side effects (no config.yaml.bak written or restored) and probed:
- memory -
memory.additional_promptlonger thanmemory.additional_prompt_max_charsis a warning at the key: the memory subagent reads the cut text; - paths -
sessions.dir,logger.file,memory.dirand the scheduler's jobs folder are fine when missing as long as they can be created (the process makes them at start), and an error when a regular file stands in the way;prompts.dirhas to exist, and a template missing from it is a warning;skills.dirs,subagents.dirsandhooks.filesentries you wrote are warnings when missing, while absent defaults stay quiet; a hook file that exists has to parse;swarm.tlsmust load and everydial.ca_filemust hold a certificate; - LLM providers - each provider is asked for its model list, which exercises the address, the proxy and the credential in one request (
coddy providers logincredentials included); a provider aimed at a vendor's official endpoint with nothing to present is reported without a request. Everymodels[]entry is then checked against that list: a model the server does not name is a warning, since some servers serve more than they list. Amax_tokenson acodexmodel is a warning whatever the provider answers, since no request carries it; - MCP servers of
~/.coddy/mcp.json- the executable of a stdio server is resolved inPATHthe way the spawn would, without spawning it; a remote server is asked for any HTTP answer, with its headers. Project-local.coddy/mcp.jsondeclarations are not contacted: they sit behind the workspace trust gate; - Telegram - when
gateways.telegram.enableis true the token is checked against the Bot API (getMe), throughgateways.telegram.proxywhen set; the report names the bot; - remotes - each
httpserver.remotes[]URL is asked for an answer (a warning when down, since it is used only on request), and the--remotetarget of a console oracprun has to accept the token; coddy serveonly - the subsystems the configuration and the typed flags enable are resolved as a start would (a surface this binary was not built with is an error, not a silent skip), each listen address is bound once and released, so a port another process holds is named together with the line that set it, and the relays inswarm.joinand the upstreams a relay mounts are reached through their dial settings.
On its own the flag is quiet: it prints the problems - each warning and error with the place in the file and the fix - and one status line at the end, so a healthy setup answers with that line alone and a deploy script has one thing to read:
$ coddy --dry-run
dry run: 0 errors, 0 warnings, 4 ok
When something is off, the problems come first and the status line still closes the report; the exit status is 1 and the last line says dry run failed:
$ coddy --dry-run
warning skills.dirs[0]: /home/me/.coddy/skills does not exist
at /home/me/.coddy/config.yaml:22:10
fix: create it or remove the entry; a ${CWD} entry is resolved per session, so a folder missing here may exist in another workspace
warning skills.dirs[1]: /opt/team-skills does not exist
at /home/me/.coddy/config.yaml:22:34
fix: create it or remove the entry; a ${CWD} entry is resolved per session, so a folder missing here may exist in another workspace
error mcp.json[tickets]: command "ticket-mcp" not found in PATH
fix: install it or write an absolute path as the command of tickets in /home/me/.coddy/mcp.json
warning models[local/llama-4]: not in the model list of provider local (the server may still serve it)
at /home/me/.coddy/config.yaml:11:5
fix: check the model id; the provider lists gpt-oss-20b, qwen3.6-35b
error providers[gpu]: cannot reach http://127.0.0.1:18732/v1: dial tcp 127.0.0.1:18732: connect: connection refused
at /home/me/.coddy/config.yaml:6:5
fix: check api_base and that the server is running
dry run: 2 errors, 3 warnings, 4 ok
dry run failed
Add --test-config to see the whole picture: the config check report first (the same one -t prints, valid included), then every probe, the ones that passed too, so the report shows what was actually tried and against which address:
$ coddy --dry-run --test-config
/home/me/.coddy/config.yaml: valid
ok sessions.dir: /home/me/.coddy/sessions will be created at first start
warning skills.dirs[0]: /home/me/.coddy/skills does not exist
at /home/me/.coddy/config.yaml:22:10
fix: create it or remove the entry; a ${CWD} entry is resolved per session, so a folder missing here may exist in another workspace
warning skills.dirs[1]: /opt/team-skills does not exist
at /home/me/.coddy/config.yaml:22:34
fix: create it or remove the entry; a ${CWD} entry is resolved per session, so a folder missing here may exist in another workspace
ok mcp.json[context7]: command "npx" resolves to /usr/bin/npx
error mcp.json[tickets]: command "ticket-mcp" not found in PATH
fix: install it or write an absolute path as the command of tickets in /home/me/.coddy/mcp.json
ok providers[local]: openai at http://127.0.0.1:18731/v1 lists 2 models
at /home/me/.coddy/config.yaml:3:5
ok models[local/qwen3.6-35b]: listed by provider local
warning models[local/llama-4]: not in the model list of provider local (the server may still serve it)
at /home/me/.coddy/config.yaml:11:5
fix: check the model id; the provider lists gpt-oss-20b, qwen3.6-35b
error providers[gpu]: cannot reach http://127.0.0.1:18732/v1: dial tcp 127.0.0.1:18732: connect: connection refused
at /home/me/.coddy/config.yaml:6:5
fix: check api_base and that the server is running
skipped models[gpu/qwen3.6-35b]: provider gpu failed
dry run: 2 errors, 3 warnings, 4 ok
dry run failed
ok and skipped lines carry no fix; a warning never fails the run; an error does. A file that fails the static check is always shown, whichever flags were given: nothing else can be probed until it is fixed. Network probes run concurrently and each is bounded to ten seconds, so a dead server costs one wait, not one per model. Secrets are not echoed: a Telegram token is masked in any error text and a provider key is never printed. CODDY_TELEGRAM_API_BASE points the Telegram probe, and the bot itself, at another Bot API origin: a self-hosted server, or the offline stand of cmd/tgfake.
Agent name, title, and build version are not configurable here. They are fixed in the binary and reported during ACP initialize (internal/acp and internal/version).
# LLM backends (Go: []config.ProviderConfig, internal/config/providers.go)
# Each providers[].name must match ^[a-zA-Z][a-zA-Z0-9_-]*$ (ASCII letter first, then letters, digits, hyphen, underscore).
# api_key may be a literal, "${ENV}" expanded when the file loads, or empty to read NAME_API_KEY at LLM call time
# (NAME is the provider name in uppercase with hyphens mapped to underscores, for example rpa -> RPA_API_KEY).
# api_key_command (optional): when api_key is empty, this command is run via the detected host shell and its trimmed stdout is
# used as the key (credential helper, like git/docker helpers or AWS credential_process). It lets a provider fetch
# short-lived or login-issued keys without storing a static secret. On failure resolution falls back to NAME_API_KEY.
# Resolution order: literal api_key -> api_key_command stdout -> NAME_API_KEY env.
# A codex row reads none of the three and runs no helper: it signs in with ChatGPT (coddy providers login).
providers:
- name: "openai"
type: "openai"
api_key: "${OPENAI_API_KEY}"
# api_base: "" # optional override for OpenAI-compatible base URL
# api_key_command: "my-cli print-token" # host shell: pwsh/powershell/cmd on Windows; bash/sh elsewhere
# proxy: none # route of this row: inherit (default), none, or a proxy URL
# timeout_ms: 300000 # optional bound on each LLM request incl. streamed read (0 = no client timeout)
- name: "anthropic"
type: "anthropic"
api_key: "${ANTHROPIC_API_KEY}"
- name: "neuraldeep"
type: "neuraldeep"
api_key: "${NEURALDEEP_API_KEY}"
# In the bundled web UI, select codex and use Sign In with ChatGPT. Tokens are
# stored at $CODDY_HOME/providers/codex/codex-auth.json, not in config.yaml.
- name: "codex"
type: "codex"
# `coddy providers login devin` signs in to a Devin account in the browser
# (or reuses the Devin CLI login with --devin-cli) and adds this row and one
# model per family. The session token lives under $CODDY_HOME/providers/devin/.
- name: "devin"
type: "devin"
- name: "local"
type: "openai"
api_base: "http://localhost:11434/v1"
api_key: "~"
- name: "deepseek"
type: "openai"
api_base: "https://api.deepseek.com/v1"
api_key: "${DEEPSEEK_API_KEY}"
# Logical models (Go: []config.ModelEntry, internal/config/models.go).
# Each model value is "provider_name/api_model_id". The first path segment must match providers[].name.
# The same string is the ACP model selector and agent.model default.
models:
- model: "openai/gpt-5.6-terra"
max_tokens: 8192
reasoning_default: medium # reasoning models take a level, not a temperature
multimodal: true # accepts images/files; UI shows file attachment button
- model: "anthropic/claude-3-5-sonnet-20241022"
max_tokens: 8192
temperature: 0.2
multimodal: true
- model: "openai/gpt-5"
max_tokens: 8192
reasoning_default: medium # level pre-selected for new chats (composer reasoning selector)
# reasoning_levels: [low, high] # optional override of offered levels; [] hides the selector
# allow_reasoning_off: true # optional: add Off only after verifying this deployment honours the disable-reasoning request
- model: "local/qwen2.5-coder:14b"
max_tokens: 4096
temperature: 0.1
- model: "deepseek/deepseek-coder-v2"
max_tokens: 8192
temperature: 0.1
- model: "neuraldeep/default"
max_tokens: 8192
temperature: 0.2
- model: "codex/gpt-5.6-sol"
max_tokens: 8192
- model: "devin/claude-sonnet-5"
reasoning_levels: [low, medium, high, xhigh, max] # each level is a variant of the family
reasoning_default: medium
# ReAct loop settings (Go: config.Agent, internal/config/agent.go)
agent:
model: "openai/gpt-5.6-terra" # optional default LLM until the client overrides per session;
# unset, interactive surfaces pick a model per session, while
# coddy -p / coddy acp / API calls without a model report "no model configured"
max_turns: 165 # ReAct iterations per prompt, recoveries included; 0 explicitly disables the limit
llm_retry_max: 3 # shared per-step budget: transport retries + no-answer recoveries
# (default 3; 0 disables these retries, not separately configured continuations)
llm_retry_base_ms: 1000 # initial backoff between LLM retries; a server-provided
# pause (Retry-After-Ms / Retry-After headers, "Limit resets
# at" / "retry in Ns" body phrases) overrides the backoff,
# capped at 60s
llm_min_interval_ms: 0 # min gap between consecutive LLM calls, retries included; e.g. 12000 on strict free tiers
llm_first_token_timeout_ms: 90000 # cancel a silent streamed LLM call after this long (0 disables the guard)
llm_stream_idle_timeout_ms: 300000 # cut a streamed answer that sends nothing for this long after its first bytes,
# keeping the text already delivered (0 disables the guard; blocking models are never guarded)
wait_for_limit_reset: false # wait for a hit usage limit to lift and re-issue the call (off: the turn ends with the error)
wait_for_limit_reset_max_ms: 14400000 # total wait per turn (4 h), the retry wrapper's sleeps on a limit included; under 60 s it also bounds ordinary 429 retries; 0 never waits
loop_guard: true # stop a response that repeats itself, and a tool called over and over with identical args
loop_tool_repeat_limit: 2 # identical calls in successive ReAct responses before the guard steps in (0 disables)
loop_stream_repeat_cycles: 5 # identical output cycles in one stream before it is cut (0 disables)
loop_nudge_max: 1 # nudges before the guard stops the turn with a notice
# System prompt templates
prompts:
# Empty dir = use embedded defaults. Otherwise a directory containing the files named below.
#
# Whatever you put here, Coddy prepends its identity line ("You are Coddy, ...") unless the
# template already opens with it — gateways attribute traffic by the start of the system
# prompt. Source: internal/prompts/identity.go, rationale: docs/contributing/react-agent.md (Agent identity).
#
# Go text/template data. Fields in internal/prompts/loader.go. YAML shape is config.Prompts in internal/config/prompts.go.
# {{.CWD}} - session working directory
# {{.Tools}} - markdown list of tool names and short descriptions for the current mode
# {{.Skills}} - markdown block for active skills (omit section when empty via {{if .Skills}})
# {{.Rules}} - the AGENTS.md and DESIGN.md of the agent home and of the workspace, then the always-on rules
# {{.Instructions}} - the files of instructions.files. A template that prints neither block still gets the
# documents and those files: in {{.Instructions}} when it has it, otherwise appended after it
# {{.TodoList}} - current session todo checklist as markdown lines (empty until coddy todo tools update state)
# {{.Memory}} - session notes. The memory subagent's report is not rendered here: it travels in the <turn_context> block
# {{.UTCNow}} - date and time in UTC (RFC3339), refreshed whenever the system prompt is rendered
#
# Built-in templates order: Tools, Skills, Memory (session notes).
# They deliberately render neither {{.TodoList}} nor {{.UTCNow}}: both move between the steps of a
# turn, and the system prompt is what the provider's prompt cache keys the whole conversation on.
# Coddy sends the clock and the checklist after the history instead, in a <turn_context> block,
# and a rule a tool call activates rides in that call's result. Your own template may still render them, at the cost of that cache.
# See docs/contributing/react-agent.md (The turn context block).
dir: ""
agent_prompt: "agent.md" # optional; default agent.md
plan_prompt: "plan.md" # optional; default plan.md
ask_prompt: "ask.md" # optional; default ask.md
# Session bundle storage (Go: config.Sessions, internal/config/sessions.go)
sessions:
# Empty = default $CODDY_HOME/sessions. Supports ${CODDY_HOME} and ~ in path.
dir: ""
# Context compaction (Go: config.Compaction, internal/config/compaction.go).
# Summarizes history older than the keep-recent boundary into one transcript row;
# later LLM prompts replay only the summary plus the kept tail. Trigger manually
# with the built-in /compact command (optional --model <id> for that one summary, then
# optional trailing summarizer instructions)
# or automatically at threshold_percent of the model's context window
# (models[].max_context_tokens, else the window the provider reports, else 128000).
compaction:
enable: true # master switch (manual command and automation)
auto_enable: true # false disables only automatic compaction
threshold_percent: 80 # auto-compact trigger, 1..100, a percent of the context window
keep_recent_turns: 2 # last N user turns stay verbatim; 0 summarizes everything
model: "" # models[].model for the summarizer; empty = session model
fallback_models: [] # tried in order when the summarizer above them fails; the session
# model is the last resort whether or not it is listed
# Optional long-term memory subagent (Go: config.MemoryConfig, internal/config/memory.go; logic in external/memory).
# Linked with the memory build tag; enable at runtime with memory.enable. Every user turn then starts a memory
# subagent in the background task pool (docs/features/memory.md).
memory:
enable: false
# Exact id from models[]. The memory subagent runs on it; the main assistant model is unaffected.
# Example: "rpa/qwen3.6-35b-a3b". Empty means the session's model.
model: ""
dir: "" # long-term memory root; empty = $CODDY_HOME/memory. Supports ${CODDY_HOME} and ~ when set.
wait_seconds: 20 # how long a turn waits for the report before its first model call; 0 never waits
timeout_seconds: 300 # hard limit of one memory run
keep_runs: 20 # finished memory runs kept per session in the Tasks panel; 0 keeps all
recall_max_turns: 6 # the child's round cap is the larger of the two
persist_max_turns: 12
copilot_max_tokens: 4096
max_search_hits: 8
max_note_chars: 900 # longest body one saved note may have, in characters; 0 = no cap
additional_prompt: "" # your own instructions for the memory subagent only; the main agent never sees them
additional_prompt_max_chars: 0 # cut additional_prompt at this many characters (a warning is logged); 0 = no cap
# Skills directories (Go: config.Skills, internal/config/skills.go)
skills:
# Four folders are always read, lowest -> highest priority, whatever this
# key says (a lower folder wins a skill name over the ones above it):
# ${HOME}/.agents/skills - your skills shared with every agent (npx skills / npx skillsbd)
# .agents/skills - the project's skills shared with every agent
# ${CODDY_HOME}/skills - Coddy's own: the standard delivery, installed skills
# .coddy/skills - the project's skills for Coddy
# dirs only ADDS directories, read after the four and stronger than them.
# ${HOME} and ~ are your home, ${CWD} and a relative path the session's workspace.
dirs:
- "~/my-team-skills"
# Rules (Go: config.Rules, internal/config/rules.go)
# One project folder is read under the session CWD: the first of .coddy/rules,
# the shared .agents/rules, .cursor/rules, .claude/rules and .codex/rules that
# holds a rule file, so another agent's mirror of the same rules is not loaded
# twice. Your own ~/.coddy/rules joins it in every workspace. The AGENTS.md and
# DESIGN.md documents - yours in ~/.coddy, the workspace's, and the nested ones
# of the folders a tool enters - are read whatever these keys say and have no
# key here. .mdc files are read as Cursor rules, .md files as Claude Code rules.
# The rules that always apply go into {{.Rules}} in the system prompt (separate
# from skills); a rule scoped to paths arrives with the tool result or message
# that touches a matching path. See docs/features/rules.md.
rules:
auto_discover: true
systems: [] # optional: user, coddy, agents-dir, cursor, claude, codex ("agents" no longer affects the AGENTS.md files; alone it loads no rule folder)
# MCP servers are not declared here: they live in ~/.coddy/mcp.json (every
# session) and <workspace>/.coddy/mcp.json (that project, once approved), a
# Cursor-compatible "mcpServers" object. An old mcp_servers list is moved into
# ~/.coddy/mcp.json on the next start. See docs/features/mcp.md.
# Tool configuration (Go: config.Tools, internal/config/tools.go)
tools:
# Controls when the agent asks for user approval before running tools.
# ask - always prompt for commands and file writes (default)
# accept_edits - auto-approve file writes; prompt for shell commands
# bypass - never ask for permission (use only in trusted environments)
# Overridable per session (ACP session/set_config_option "permission_mode", /permissions,
# the web composer chip); the override lives in memory, a restart comes back to this value.
permission_mode: ask
# TCP dial timeout for SSH connections in seconds (default: 30).
# ssh_connect_timeout: 30
# Subagents (Go: config.Subagents, internal/config/subagents.go). Child agents the model spawns with spawn_agent
# from markdown definitions; each run is a background task with its own child session. See docs/features/subagents.md.
# subagents:
# enable: true
# dirs: [] # extra folders after ~/.agents/agents, .agents/agents,
# # ${CODDY_HOME}/agents and .coddy/agents, which are always read
# project_trust: ask # ask (approve project files once per workspace) | allow | deny
# max_concurrent: 4 # subagent runs in flight across the whole process
# max_depth: 1 # 1 = children cannot spawn further; 0 = nobody spawns
# default_timeout_seconds: 1800 # hard limit when the definition and the call give none
# max_turns: 0 # 0 follows agent.max_turns
# Hooks (Go: config.Hooks, internal/config/hooks.go). Your own commands at lifecycle points of a session,
# defined in JSON files of Claude Code's shape; project files need a one-time approval. See docs/features/hooks.md.
# hooks:
# enable: true
# files: ["${CODDY_HOME}/hooks.json", "${CWD}/.claude/settings.json", "${CWD}/.claude/settings.local.json", "${CWD}/.coddy/hooks.json"]
# project_trust: ask # ask (approve project files once per workspace) | allow | deny
# default_timeout_seconds: 60 # per hook process when the definition gives no timeout
# stop_loop_limit: 5 # Stop-hook continuations per turn
# max_output_chars: 10000 # cap on what one hook hands to the model or the user
# HTTP OpenAI gateway (only with go build -tags=http). Embedded SPA on / needs -tags=http,ui too. See docs/reference/http-api.md
# httpserver:
# host: "127.0.0.1"
# port: 8080
# Cron scheduler (only with go build -tags=scheduler). UTC crontab; flat *.md jobs in ${CODDY_HOME}/scheduler
# and, once approved, in <workspace>/.coddy/scheduler.
# scheduler:
# enable: false
# project_trust: ask
# max_queue: 10
# timeout: "30m"
# retain_sessions: 5 # max completed run session dirs kept per job_id (default 5)
# Logging (Go: config.Logger, internal/config/logger.go)
logger:
level: "info" # debug | info | warn | error
# Raise or lower one subsystem on its own. A component is the dotted name a
# record carries in its "component" field (gateway, gateway.telegram,
# session, agent, scheduler); a parent name covers what is nested under it
# and the longest match wins. Omitted = every record follows level above.
levels:
- component: "gateway.telegram"
level: "debug"
# Where records go: any combination of stdout, stderr, file. Omitted or empty = stderr only.
outputs: []
# Path for the file sink; required when outputs includes file.
file: ""
# text (default) or json
format: "text"
rotation:
max_size_mb: 0 # 0 = no size-based rotation
max_files: 0 # rotated backups to keep when max_size_mb > 0ACP flags override the same knobs when set: --log-level, --log-output (stdout, stderr, file, both), --log-file, --log-format. Empty flag values keep the YAML (or built-in) defaults. --log-level also takes the per-component spec as a comma-separated list (--log-level "info,gateway.telegram=debug"); a bare level leaves the configured levels entries alone, and a spec that names components replaces them.
If the older two-field style had file set under logger but no outputs, the loader expands to stderr plus file so file logging takes effect.
The built-in ssh_run_command tool lets the agent run commands on remote hosts over SSH — no external ssh binary required (pure-Go via golang.org/x/crypto/ssh). The only configurable knob is tools.ssh_connect_timeout (TCP dial timeout, default 30 s).
Authentication order:
- SSH agent — if
SSH_AUTH_SOCKis set and reachable, the agent is used first. This covers YubiKeys, 1Password SSH agent, gpg-agent, and standardssh-agentsetups. - Key files — Coddy always looks in the current OS user's
~/.sshdirectory. Key names tried in order:id_ed25519,id_rsa,id_ecdsa,id_dsa. Keys protected by a passphrase are silently skipped.
Both sources are active simultaneously — if the agent is available and has keys, files still act as a fallback if the agent declines.
Host key verification — derived automatically from tools.permission_mode:
- Any mode except
bypass(default) — new hosts are added to~/.ssh/known_hostsautomatically on first connect (TOFU); if a known host's key has changed, the old entry is replaced with the new one. bypass— host key verification is disabled (suitable for ephemeral VMs or CI environments).
Tool schema — ssh_run_command accepts:
| Field | Type | Required | Description |
|---|---|---|---|
host |
string | yes | user@hostname — user is required |
command |
string | yes | Shell command to run on the remote host |
port |
integer | no | SSH port (default: 22) |
timeout_seconds |
integer | no | Command timeout in seconds |
permission_rationale |
string | no | Text shown in the permission dialog |
The tool requires user permission (same as run_command) and returns combined stdout + stderr.
The httpserver key (config.HTTPServerConfig in internal/config/http.go) is ignored unless you use a binary built with -tags http. It sets default host and port when coddy serve is still at the built-in flag defaults (0.0.0.0 and 12345). See docs/reference/http-api.md.
Off by default. It closes the browser surface of a server that is on a network, so transcripts, tool output and the configuration editor are not readable by whoever finds the port. Write the account with the command rather than by hand:
coddy serve set-password --user pashahttpserver:
host: 0.0.0.0
auth_token: "${CODDY_HTTP_TOKEN}" # unchanged: the credential API clients present
login:
enable: true # omit to follow the credentials; false wins over everything
user: "pasha"
password_hash: "$$argon2id$$v=19$$..." # argon2id, written by the command above
session_ttl_hours: 720 # 0 = the browser drops the cookie on close (the server still expires its record after 30 days)A hash written into this file by hand needs every $ doubled ($$argon2id$$v=19$$...), because a $NAME is expanded as an environment reference when the file loads. The command does that for you; coddy -t names the problem when it finds a hash that no longer parses. A ${VAR} reference in user or password_hash works like every other value here and survives a save from the settings screen as a reference; to keep a credential out of the document entirely, use CODDY_HTTP_USER / CODDY_HTTP_PASSWORD instead.
The account can also come from the environment alone - CODDY_HTTP_USER and CODDY_HTTP_PASSWORD, see the .env section below - which is the route for a container or a systemd unit. The form is for browsers; coddy --remote, coddy acp --remote, a swarm relay and every script still present the bearer token. Full behaviour: HTTP API, Remote mode.
The mcp.project_trust key decides whether the project-local <workspace>/.coddy/mcp.json may start
its servers: ask (default) holds them until the operator approves each declaration for that workspace,
allow starts them automatically, deny never loads them. Pass coddy acp --mcp-project-trust <value>
or coddy serve --mcp-project-trust <value> to override it for one process, which is what CI jobs and
container entrypoints use instead of editing the config file. An unknown value fails the launch.
Added for issue #80; full guide in
docs/features/mcp.md.
The scheduler key (config.SchedulerConfig in internal/config/scheduler.go) is used only when you build with -tags scheduler. Set scheduler.enable: true in YAML or pass coddy acp -scheduler / coddy serve -scheduler to set scheduler.enable for that process without editing the config file.
Jobs are flat *.md files in two fixed folders: your own in ${CODDY_HOME}/scheduler, and the project jobs a repository carries in <workspace>/.coddy/scheduler, which run only once trusted under scheduler.project_trust (ask by default; a project job you create through Coddy is approved at once, see Scheduler). The old scheduler.dir key is no longer read: the next start copies its jobs into ${CODDY_HOME}/scheduler and removes it. Each file has YAML frontmatter with description, schedule (five cron fields, UTC), optional cwd (defaults to the directory where coddy was started, or to its workspace for a project job, whose cwd must stay inside it), model, mode (agent, plan, or ask), optional agent (a subagent definition the run is made under), optional permission_mode (ask, accept_edits or bypass; empty is bypass, the unattended default), optional paused (when true, cron and manual run are skipped until resume). The markdown body is the one-shot instruction for the run, which is a background agent task under the job's own session (Scheduler). One sidecar, basename.state (the last fired slot and the job session id), sits next to basename.md for a user job and under ${CODDY_HOME}/scheduler/.projects/ for a project job.
retain_sessions (default 5) caps how many finished runs are kept per job_id (their task records and transcripts, under the job session); older runs are removed when a run finishes. max_queue caps the runs in flight across all jobs and timeout is a run's hard limit (the background task pool still caps it at tools.background.max_timeout_seconds).
When the scheduler is effectively enabled, coddy_scheduler_* tools cover list or get, create or replace or patch, delete, pause or resume, manual run, cancel, and listing run metadata (coddy_scheduler_jobs_list, coddy_scheduler_job_get, coddy_scheduler_job_create, coddy_scheduler_job_replace, coddy_scheduler_job_patch, coddy_scheduler_job_delete, coddy_scheduler_job_pause, coddy_scheduler_job_resume, coddy_scheduler_job_run, coddy_scheduler_job_cancel, coddy_scheduler_job_runs). With -tags=http,scheduler, the same operations exist as REST under /coddy/scheduler (see docs/reference/http-api.md).
Requires a binary built with -tags gateway.telegram (Telegram only) or -tags gateway (all adapters). The coddy serve subcommand reads this block.
# Messenger gateways (external/gateway/; build with -tags gateway.telegram or -tags gateway).
# Full guide: docs/surfaces/gateway.md
gateways:
telegram:
# Set to true to activate the Telegram adapter when coddy serve starts.
enable: false
# Bot token from @BotFather. Never hard-code; always use an env reference.
token: "${TELEGRAM_BOT_TOKEN}"
# How the bot reaches the Bot API: inherit (the default) follows HTTPS_PROXY,
# none connects directly, or a proxy URL (http, https, socks5, socks5h).
# proxy: "socks5h://127.0.0.1:1080"
# Telegram user IDs with admin privileges.
# Admins bypass every access check and can always interact with the bot.
admins: []
# Example:
# admins: [98874093]
# Default access level for chats without a per-chat override.
# "all" - anyone who can write to the chat
# "admins" - only user IDs listed in admins
# "group:<name>" - only users in the named user_groups entry (admins always pass)
default_access: "all"
# Default session isolation mode for group chats without a per-chat override.
# "individual" - each group member gets their own session
# "shared" - all members share one session
# "admin" - only admins can interact; all admins share one session
default_isolation: "individual"
# Named sets of user IDs for group-based access control.
user_groups: []
# Example:
# user_groups:
# - name: "devs"
# user_ids: [111222333, 444555666]
# Per-chat overrides. chat_id is negative for groups and supergroups.
chats: []
# Example:
# chats:
# - chat_id: -1001234567890
# isolation: "individual"
# access: "all"
# - chat_id: -1009876543210
# isolation: "admin"
# access: "admins"token is validated at startup when enable: true. proxy is optional and reads like a provider's: empty or inherit follows the environment's proxy (HTTPS_PROXY, HTTP_PROXY, NO_PROXY), none connects directly, a URL goes through that proxy (Provider proxy). The other fields apply defaults if omitted: default_access: "all", default_isolation: "individual".
See docs/surfaces/gateway.md for the full configuration guide, running instructions, and how to add adapters for other messengers.
If $CODDY_HOME/.env exists, it is read at startup before config.yaml is parsed. This lets you keep all secrets in one place without touching shell profiles or Docker compose environment blocks.
# ~/.coddy/.env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
TELEGRAM_BOT_TOKEN=8992982910:AAF...
CODDY_HTTP_TOKEN=a-long-random-token # bearer credential for API clients
CODDY_HTTP_USER=pasha # web UI sign-in account...
CODDY_HTTP_PASSWORD=correct-horse-battery-staple # ...enables the form on its ownCODDY_HTTP_USER and CODDY_HTTP_PASSWORD are the one pair that is not referenced from config.yaml at all: the password is hashed as the server starts, the file never sees either value, and a save from the settings screen cannot write them into it. They win over an account in the file, and httpserver.login.enable: false switches the form off with them still set.
Then in config.yaml reference them as usual:
providers:
- name: openai
type: openai
api_key: "${OPENAI_API_KEY}"Rules:
- Variables that are already set in the process environment are never overridden — the process environment always wins.
.envis a fallback only. - A missing
.envis silently ignored (not an error). - The file is resolved relative to the effective
CODDY_HOME(~/.coddyby default, or the path from--home/CODDY_HOMEenv var).
Supported syntax:
| Line form | Example |
|---|---|
KEY=value |
OPENAI_API_KEY=sk-abc |
export KEY=value |
export DEBUG=true |
| Double-quoted value | MSG="hello world" |
| Single-quoted value | PATH='no escape \n here' |
Escape sequences in "…" |
NOTE="line1\nline2" → real newline |
| Inline comment (unquoted) | KEY=val # this is ignored |
| Comment line | # full-line comment |
Values already in the process environment (e.g. set by the shell, Docker, systemd) take priority and are never changed by .env.
Any config value can reference environment variables using ${VAR_NAME} syntax.
The agent resolves these at startup.
Literal $ in a value: because expansion runs over the raw file, a value that must contain a
literal dollar sign (e.g. a proxy or API-key secret like $2y$10$…) has to double it as $$ — $$
expands back to a single $, exactly like docker-compose / envsubst. Without this, fragments such as
$2y or $10 are treated as environment-variable references and resolve to empty strings, silently
corrupting the secret. The Settings UI does this automatically for the proxy fields
(providers[].proxy and gateways.telegram.proxy), which are always treated as literal URLs and do
not support ${VAR} references; for a literal $ in api_key (which does support ${VAR}),
write $$ by hand.
A save keeps the references. The loaded configuration holds what a reference resolved to, so the
Settings UI works with the secret itself and with absolute paths. When it saves, a value written as
${VAR}, ${CODDY_HOME}/... or ~/... in the file is written back that way as long as it still
loads as the value being saved - in a single value (memory.dir) and in a list entry
(skills.dirs, subagents.dirs, hooks.files, instructions.files) alike; only a value you
changed on the screen replaces the reference. A key kept in the environment or in ~/.coddy/.env
therefore never lands in config.yaml because of an unrelated save, and a save of an untouched form
writes every value back the way the file spelled it. The same holds for what the process changes
after reading the file: a command-line flag, the relay listen address coddy serve fills in or a
pairing token from the environment is not written into config.yaml unless you change that value on
the screen, and a value another save changed after you opened the form is not put back by yours.
When the file on disk does not load at the moment of the save (a broken hand edit, a deleted file),
the save writes the configuration the server runs, as it always did. Indentation and blank lines are
not kept: a save writes the file indented by two spaces, without blank lines between sections.
Two placeholders are not environment variables:
${CODDY_HOME}- the resolvedCODDY_HOMEdirectory, substituted when the file is read.${CWD}- the session working directory. It is not substituted when the file is read: it stays in the loaded value and whatever uses the path expands it against the session that asks - skill loading, subagent and hook discovery, prompt templates (prompts.dir), MCP server command, arguments, URL, environment and headers. Onecoddy serveprocess therefore serves many workspaces, and a session rooted in a project sees that project's${CWD}/.coddy/skills(or any entry you write, such as${CWD}/.agents/skills) regardless of the directory the server was started from. Only the process-scoped locations (sessions.dir,scheduler.dir,memory.dir,logger.file) expand${CWD}against the default working directory (CODDY_CWD) at load time, since no session owns them.
An environment variable named CWD does not replace the placeholder (a bare $CWD without braces is still an ordinary environment reference, as before), and GET /coddy/config, the Settings UI, and config_get report the entry exactly as written. The placeholder is honoured only in the fields listed above; in any other string value it stays as written (prompt templates use {{.CWD}} instead). Releases up to 1.0.5 substituted ${CWD} with the process directory when the file was read, so a Settings save made in that version may have stored an absolute path such as /home/you/.agents/skills where you wrote ${CWD}/.agents/skills; put the placeholder back by hand to get per-session resolution.
Provider type values match internal/llm.NewProvider: openai, anthropic, neuraldeep, codex, devin.
YAML split:
providers:name(unique),type,api_key, optionalapi_base(base URL override for the provider SDK: an OpenAI-compatible endpoint or Ollama host without/v1fortype: openai, or an Anthropic-compatible gateway/relay fortype: anthropic; fortype: neuraldeepit selects the deployment,https://api.neuraldeep.ru/v1orhttps://api.neuraldeep.tech/v1, and any other value falls back to the first), optionalproxy(the route of every request of the row:inheritby default,nonefor a direct connection, or anhttp://,https://,socks5://orsocks5h://proxy URL; see Provider proxy), optionalusage_limits_panel(boolean, defaulttrue;falsehides the account usage panel of this row on every surface and stops the usage reads behind it, meaningful fortype: neuraldeep,type: codexandtype: devintoday).models:model(stringprovider_name/api_model_id, session selector andagent.modelvalue; first segment namesproviders[].name, remainder is the API model id),max_tokens,temperature, optionalmax_context_tokens(the model's context window: what the web UI context ring, the console context percentage and automatic compaction measure against; 0 reads it from the provider's model listing when the provider reports one, else 128000 - see Context compaction), optionalmultimodal(boolean, defaultfalse; whentruesignals that the model accepts image/file inputs — the UI exposes a file attachment button in the composer for this model only, andreadshows such a model the picture in an image file instead of refusing it, see Images), optionalreasoning_levels(string list; overrides the reasoning levels offered for this model — when omitted they are auto-detected from the API model id:gpt-5*andgpt-6*→minimal,low,medium,high, OpenAIo-series,gpt-oss*,qwen3*(qwen3, qwen3.5, qwen3.6, qwen3.8, ...) and Claude extended-thinking models →low,medium,high; an explicit empty list hides the composer reasoning selector), optionalreasoning_default(the level pre-selected for new chats; must be one of the resolved levels), and optionalallow_reasoning_off(boolean, defaultfalse; addsoffto this model's choices only when an operator has verified that its provider/model deployment honours Coddy's provider-specific disable-reasoning request). Reasoning levels map to OpenAIreasoning_effortand Anthropic extended-thinkingbudget_tokens; forqwen3*models on OpenAI-compatible providers the request also carrieschat_template_kwargs{"enable_thinking": true}so the chat-template thinking switch stays on. The Codex backend rejectsmax_output_tokens, somax_tokensis not sent forcodexproviders; it also rejects theminimaltier itsgpt-5*andgpt-6*ids would normally imply, so codex-backed models offernonein its place (in the composer selector and inGET /v1/models). Reasoning turns request summaries (summary: auto) so thinking streams, and encrypted reasoning (include: reasoning.encrypted_content) so the chain of thought is replayed across tool calls the way the Codex CLI does it. See config-reference.md for token lifetime and the startup credential report.
providers[].proxy picks the route of every request a provider row makes: its completions, model list, account usage, OAuth/device sign-in and token refresh, and sign-out revoke.
- No value, or
inherit(the default): the proxy the environment of the Coddy process names.HTTPS_PROXYcovershttps://addresses,HTTP_PROXYcovershttp://ones,NO_PROXYlists the hosts that go direct, and a loopback address is never proxied.ALL_PROXYis not read. Coddy has always behaved this way: an empty value never meant a direct connection, whatever older descriptions of the field said. none: a direct connection. The row ignores those variables, so a provider that is reachable directly keeps working when the machine's proxy is broken, stale or slow - a corporate proxy that inspects TLS, a local forwarder, a variable left over from another setup.- A proxy URL (
http://,https://,socks5://,socks5h://): every request of the row goes through that proxy,NO_PROXYhosts and loopback addresses included. With either SOCKS scheme the proxy resolves host names. Credentials go in the URL (http://user:pass@host:3128), and a$in them survives a settings save (see Environment variable references).
providers:
- name: corp # needs the machine's proxy: nothing to set
type: openai
api_key: "${CORP_API_KEY}"
- name: local # reachable directly, HTTPS_PROXY or not
type: openai
api_base: http://192.168.1.20:8000/v1
proxy: none
- name: remote # a proxy of its own, whatever the environment says
type: anthropic
api_key: "${ANTHROPIC_API_KEY}"
proxy: socks5h://127.0.0.1:1080The setting belongs to its row alone, so rows with different routes live side by side in one file. The Telegram gateway's gateways.telegram.proxy takes the same values for the bot's own requests (Telegram gateway). It covers the requests the Coddy process sends; the page a sign-in opens in your browser and whatever an api_key_command does are outside it. A changed value applies to the requests the row starts after it; a sign-in already waiting for the browser finishes on the route it started with. The environment variables are read once per process, so a change to them needs a restart. The keywords are accepted in any case and written back in lower case; anything else that is not a proxy URL is a configuration error that names the accepted values (the direct of an http_request call is not one of them). In the web UI the row shows the Ignore system proxy switch, which writes none, above the Proxy URL field (Web UI). When a provider cannot be reached, coddy --dry-run names the route the request took in its hint - the row's proxy, a direct connection, or the proxy the environment named - with the credentials left out.
Standard OpenAI API. Supports the current reasoning families (gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5) as well as the older o-series and gpt-4 ids.
Provider needs api_key. Optional proxy routes this row's requests (Provider proxy). The models[].model string must start with this provider name and a slash, then the OpenAI API model id, for example openai/gpt-5.6-terra. Also set max_tokens, and temperature for the non-reasoning ids.
Anthropic API. Supports: claude-3-5-sonnet-*, claude-3-5-haiku-*, claude-3-opus-*
Provider needs api_key. Optional api_base overrides the Anthropic API base URL (default https://api.anthropic.com), for example an Anthropic-compatible gateway or relay. Optional proxy routes this row's requests (Provider proxy). Use models[].model like anthropic/claude-3-5-sonnet-20241022, plus max_tokens, temperature.
NeuralDeep API via its OpenAI-compatible endpoint.
Credentials come from either a hub sign-in or a plain key. coddy providers login neuraldeep signs in through the device flow: it prints a short code and the hub page that confirms it, so the browser can be on any machine - a laptop, a phone - while Coddy runs on a server reached over SSH. It opens that page for you when this machine has a browser of its own. --browser asks for the older loopback callback instead; that one only completes in a browser running on this machine. A login waits for the confirmation until the hub's fifteen-minute deadline, and Ctrl-C ends it sooner. Either way it stores the hub-issued key under $CODDY_HOME/providers/<name>/neuraldeep-auth.json, and appends the tier's models to the config; the bundled web UI offers Sign In with NeuralDeep on the provider row. An explicit api_key (or api_key_command / NEURALDEEP_API_KEY) always wins over the stored login.
While a model of a provider with a usage source is active, the console footer, the remote console and the HTTP API show the account's usage, refreshed at session start and after every turn; see docs/surfaces/console.md (Footer, /usage) and docs/reference/http-api.md (GET /coddy/providers/{name}/usage). The sources today: type: neuraldeep reads the hub's read-only GET /v1/limits (session and week windows as percent used with reset times, the wallet in rubles, a hit limit with its reset time); type: codex reads the Codex backend's usage endpoint with the saved ChatGPT OAuth token (the plan and percentage windows by their upstream durations, plus feature-scoped entries); type: devin reads the seat-management status RPC of the Devin API server (the plan, and daily/weekly quota windows as percent used for quota plans or an ACU meter for ACU plans). The row's own credential is used, no dollar figure and no invented request counts are ever shown. The panel is on by default; usage_limits_panel: false on the row (the Usage limits panel switch in Settings → LLM Providers) hides it on every surface and stops the usage reads for that row, for a shared screen or an account that is not yours to watch.
The same API is served from two deployments: https://api.neuraldeep.ru/v1 for Russia and https://api.neuraldeep.tech/v1 for everywhere else. api_base selects one - leave it empty for the first, and any value that is not one of the two falls back to it (a startup warning says so). The choice travels with the credential: sign-in goes to hub.neuraldeep.ru or hub.neuraldeep.tech to match, so pick the endpoint before signing in (coddy providers login neuraldeep --api-base https://api.neuraldeep.tech/v1, or the endpoint dropdown in Settings). A login with --api-base also moves an existing provider row to that endpoint (unless --no-config), so the row and the key agree; in Settings the sign-in follows the dropdown as picked in the form, before Save. A key minted by one hub is not honored by the other; Coddy warns at startup when the stored login and the selected endpoint disagree, and the Settings row shows the same warning live. CODDY_NEURALDEEP_BASE_URL and CODDY_NEURALDEEP_HUB_URL still redirect the whole process for stands and tests, and they win over the config. Optional proxy routes this row's requests, the hub sign-in and the usage reads included (Provider proxy). Use models[].model like neuraldeep/qwen3.6-35b-a3b, plus max_tokens, temperature.
The models of a Devin (Cognition) account, reached the way the Devin CLI reaches them.
coddy providers login devin signs in through the browser (PKCE, like devin auth login): the Devin page sends the browser back to a loopback port on this machine, and over SSH you paste the address it ended on into the terminal instead. --devin-cli reuses the login the Devin CLI already holds and opens no browser. The session token is stored under $CODDY_HOME/providers/<name>/devin-auth.json; without it the provider falls back to the Devin CLI's credentials.toml, and an explicit api_key (or api_key_command / DEVIN_API_KEY) wins over both. The login adds one model per family, such as devin/claude-opus-5, with the family's variants as its reasoning_levels: level high is sent as claude-opus-5-high. api_base is ignored; optional proxy routes the sign-in, the catalog and chat (Provider proxy). The full story, including how levels map to variants and how the output cap is chosen, is on Devin.
A row is a profile: several rows may share a type, each under a name of its own, and each keeps its own sign-in, its own models and its own usage. Three ChatGPT accounts are three codex rows, two NeuralDeep accounts are two neuraldeep rows:
providers:
- name: "codex"
type: "codex"
- name: "codex-work"
type: "codex"
- name: "neuraldeep"
type: "neuraldeep"
- name: "nd-tech"
type: "neuraldeep"
api_base: "https://api.neuraldeep.tech/v1"
models:
- model: "codex-work/gpt-5.5"
- model: "nd-tech/qwen3.6-35b-a3b"Each row signs in separately: the Sign In button on its row in Settings, or coddy providers login <name> in a terminal. A row config.yaml does not list yet is created by its login when --type names the type (coddy providers login codex-work --type codex). The login lands under $CODDY_HOME/providers/<name>/, a model of the row is <name>/<model id>, and the row's <NAME>_API_KEY variable (ND_TECH_API_KEY) belongs to that row only; a codex row reads no key variable at all.
The Codex CLI login (~/.codex/auth.json, CODEX_HOME) and the Devin CLI login are one account each, so each stands in for one row without a login of its own: the only row of its type, or, when there are several, the row named codex (devin). Every other row signs in itself instead of quietly running on that account - adding a second codex row to a setup whose only row, chatgpt, ran on the Codex CLI login leaves chatgpt unsigned too, until it signs in or is renamed codex. The startup log, coddy --dry-run, coddy providers list and the Settings row name such a row and the row the CLI login serves, and --devin-cli refuses a row the Devin CLI login does not serve.
The rows stay apart where they are shown too: the usage panel heading and the console's /usage name the row next to the brand (Codex · codex-work) unless the row is named after its type, and a NeuralDeep sign-in labels its key on the hub with the row (coddy @ host (nd-tech)).
Use type: openai and set api_base to an OpenAI-compatible base URL that already includes /v1, for example http://localhost:11434/v1 for Ollama.