Skip to content

Configuration

~/.config/colinear/config.json (legacy ~/.colinear.json also works); LINEAR_API_KEY env var covers the key. View and edit live with :config — e opens $EDITOR, seeds the file from current settings on first use, and changes hot-apply on return. A config file that exists but doesn't parse is a startup error rather than a silent fallback to defaults.

{
  "linearApiKey": "lin_api_...",
  "repos": [
    {
      "name": "cloud",
      "path": "~/work/cloud",
      "description": "the control plane: API servers, background workers, infra",
      "defaultBranch": "main",
      "worktreeRoot": "~/work/cloud-worktrees",
      "checks": [{ "name": "fmt", "cmd": "bin/fmt --check" }]
    },
    { "name": "engine", "path": "~/work/engine", "pushRemote": "my-fork" }
  ],
  "team": "CLOUD",
  "concurrency": 3,
  "model": "sonnet",
  "guidance": {
    "general": [
      "PRs should solve the problem at hand simply and clearly.",
      "Code is debt: don't write code that isn't needed to solve and validate the problem."
    ],
    "review": "Flag missing tests, but don't ask for tests that only restate the implementation."
  },
  "prSignoff": "_written by claude on behalf of @jubrad_",
  "prSignoffScope": "body",
  "notifications": true,
  "stateSync": true,
  "ciAutofix": true,
  "autoRebase": false,
  "retentionDays": 30,
  "worktreeRetentionDays": 7,
  "attachPermissionMode": "auto",
  "terminal": "ghostty",
  "tickMs": 1000
}

Config reference

Every key is optional except a Linear API key (config or env). Defaults are what you get by leaving a key out.

key default what
provider "linear" which issue tracker this context talks to: linear, or sqlite for a local one. Details
sqlitePath <state dir>/local.db where the sqlite tracker's file lives
demo false scripted agents, fabricated board, no network. Details
linearApiKey $LINEAR_API_KEY Linear personal API key. Leave it out of the file and export the env var if you'd rather not have it on disk
repos one repo (see below) the allowlist — agents only ever touch these, and only through worktrees. First entry is the default. Details
team your assigned issues Linear team key ("CLOUD") to browse, or "all" for every team. --team CLOUD / --team all override it for one run, and the last team picked with t is remembered
concurrency 3 agent sessions running at once. Above ~5 you start hitting subscription rate limits
agent "claude" the runtime for a model no runtime recognises. Most model names say which runtime they belong to on their own, so this is rarely worth setting. Details
model Claude Code's default model for agents ("opus", "sonnet", "fable", "haiku"). The names are resolved by the Claude Code bundled with the agent SDK, so an alias means whatever that build considers current — "opus" is Opus 5.5 and "fable" is Fable 5.1 as of SDK 0.3.280 — and an exact id such as "claude-opus-5" pins one version. Note that an alias moves when the SDK is bumped: "opus" meant Opus 5 before 0.3.280. Overridable per dispatch (c) and per task (m). Takes a map to run different work on different models. Details
fallbackModel "default" what a session demotes to when its model is overloaded or out of allowance. "default" is Claude Code's own alias for the model it would otherwise have picked, which is the useful answer when model pins an expensive one. Takes a comma-separated list, tried in order; "" turns it off. Scoped per kind of session the same way as model, above. It covers two different failures. An overloaded model is handled by the agent SDK, which retries your primary at the start of every turn so a passing spike doesn't strand the session on the cheaper model. A spent allowance is handled by colinear itself, because the SDK's fallback does not cover it: Claude Code answers You've hit your monthly spend limit. Switch to another model to continue. and the session is re-run on the next model in the list, with a line on the card saying so. A fallback naming the model the session is already on is dropped rather than passed on, since the SDK refuses that pairing outright
guidance none standing house rules injected into agent prompts, globally or per prompt. Details
prSignoff none markdown appended to what the agent wrote when colinear posts it on a PR, so the author knows what wrote it. Your own comments are never signed: each finding records who wrote it, and s in the compose pane flips that. Details. String or list of lines. An empty comment never becomes a signoff-only comment
prSignoffScope "all" "all" signs each of the agent's inline comments, and the body when it carries the agent's words. "body" signs only the body, whenever the agent wrote any of the review, so a review with six findings carries one attribution instead of seven
notifications true macOS notifications for the events that want you: a task needs input, finishes or fails, a CI-fix or rebase session goes out, a pre-review is ready. Click-through to the PR/issue with terminal-notifier installed
stateSync true move Linear states automatically: dispatch → In Progress, first PR → In Review
ciAutofix true dispatch a fix session when a task's PR checks go red (one per red rollup, re-armed when it goes green)
autoRebase false default for auto-rebase on conflict; the m modal overrides it per task
autoDispatchSubs false when a tracking parent gains a sub-issue nobody has started, dispatch it. Details
todo on :todo: false switches it off; an object sets how it ranks: the policy its agent is given, the review turnaround, what counts as stale, and an optional refresh clock. Details
retentionDays 30 how long finished work stays on the board. Details
worktreeRetentionDays 7 how long a finished task's worktree is kept before coli gc offers it. Details
experimental false master switch for unfinished features. Nothing in experiments runs unless this is true. Details
experiments none per-feature opt-in: { "coordination": true }, or a list of names
agentPermissionMode "auto" what headless agents may do on their own: auto, acceptEdits, default, plan, bypassPermissions. Details
denyTools none tools and command patterns no agent may use, applied as policy from outside the repo. Details
attachPermissionMode "auto" permission mode for s attach sessions: auto (classifier gates risky commands), acceptEdits, bypassPermissions, default. Headless agents always run auto; classifier-blocked commands surface on the board as allow/deny questions
autoDispatchLabels unset per-team dispatch labels: {"CLOUD": "agent", "SAS": ["colinear", "bot"]}. Issues in a listed team carrying that team's label dispatch themselves — swept every minute, 3 per sweep across all teams, through the normal pipeline (triage included). The map is the scope: a team with no entry is opted out, and each team names its own label because labels are not namespaced. See dispatch
editor unset what e opens — answer forms, review documents, the config itself. Beats $EDITOR (the precedence git gives core.editor); flags welcome: "code --wait". Unset falls through to $EDITOR, then vi
remote unset (local) run this context against a daemon on another machine: {"ssh": "vm"}. Add "forward": true and colinear opens (and owns) the ssh tunnel to its socket; "socket" names the far-side path instead of asking for it. Details
terminal in-place where s attaches: unset hands over the current terminal (recommended), "ghostty" / "terminal" open an external window
tickMs 1000 UI refresh tick. Raise it (e.g. 2000) if your terminal or multiplexer flickers

Legacy single-repo keys still work in place of repos: repo, defaultBranch, worktreeRoot, checks.

Environment: LINEAR_API_KEY, COLINEAR_CONTEXT (contexts), COLINEAR_STATE_DIR (points state, socket, pidfile and log somewhere else — used to isolate test runs), EDITOR, SHELL.

Repos

Agents only ever touch repos on this list, and only through worktrees under each repo's worktreeRoot — your working copy is never modified (the main checkout only sees git fetch and git worktree add). The first entry is the default; c (custom dispatch) and m (edit task) pick per task, and triage can re-route an issue on its own.

key default what
path — required; ~ expands
name basename of path how the repo is named in the UI and in dispatch modals
description none what lives here. Triage reads these to route each issue to the right repo, so write them honestly
defaultBranch "main" branch worktrees are cut from
remote "origin" upstream git remote name — worktree base, and the repo PRs land in
pushRemote = remote remote branches are pushed to. Set your fork here for a fork workflow ("jubrad")
prBase = defaultBranch branch PRs are opened against
worktreeRoot <path>-worktrees where per-issue worktrees are created
checks none commands run in the worktree after the work pass: [{ "name": "fmt", "cmd": "bin/fmt --check" }]. Output lands on the task detail view

remote / pushRemote are git remote names as they appear in git remote -v for that repo ("mz", "jubrad") — not owner/repo slugs. In fork mode agents skip stacked PRs, since those would require pushing to the upstream.

Agent runtimes

agent picks what actually runs a session. Both need their own CLI installed and logged in, and neither should be given an API key — the runs bill the subscription behind the logged-in CLI.

claude codex
CLI claude codex
models sonnet, opus, fable, haiku gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-6-astra
can stop and ask you yes, AskUserQuestion yes, by convention
M reaches a session yes yes, as the next turn
s opens it in a terminal yes yes
enforces denyTools yes no, sandbox modes only
reports a price yes no, tokens only

The two "no" answers matter more than the rest.

How Codex asks is different. Codex can ask you things interactively, but not over the non-interactive path colinear drives: codex exec answers its own ask-the-user request with "not supported in exec mode". So colinear supplies the mechanism instead. A Codex agent is told that ending a turn with NEEDS INPUT: <question> is how it asks, that becomes a real question on the card, and your answer becomes its next turn. Prompts still say "use AskUserQuestion" and the adapter translates, so nothing about the prompts is runtime-specific. The practical difference is that a Codex question is one free-text question rather than up to four with multiple-choice options.

Codex does not enforce denyTools. It has sandbox modes rather than per-tool rules, so a rule like Bash(git push --force:*) has nothing to attach to. Colinear runs it in workspace-write, which confines writes to the worktree it was handed. If your deny list is load-bearing, that is a reason to keep work on claude.

Codex reports no price. Its runs show -- in :costs instead of a dollar figure, and the header says how many sessions are unpriced. Tokens are reported by both, so tokens are the measure that compares across them.

Running both at once

The model names its own runtime. There is no second map to keep in step — you pick a model and the runtime follows:

{
  "model": { "general": "fable", "review": "gpt-5.6-sol" }
}

Reviews run on Codex there, everything else on Claude Code, because gpt-5.6-sol is Codex's name and fable is Claude Code's. Each runtime claims its own names, including exact ids like claude-opus-5-5 and gpt-6-astra.

This is also what makes it work per task, not just per kind. m and c offer every runtime's models, so sending one task to Codex and leaving the next on Claude is one keypress, and the runtime moves with the model rather than being a separate thing to remember.

For a name nothing recognises, say which runtime with a prefix:

{ "model": { "work": "codex/some-new-model" } }

Failing both, agent is the default. A fallback on a different runtime is dropped: a session cannot demote across runtimes, and carrying one would fail only once the first model ran out.

Attaching follows the conversation rather than the config. A session records which runtime ran it, so s on a review started under Codex opens codex resume even when everything else is Claude. Rows written before this have no recorded runtime and fall back to the config, which is what it was.

Models per kind of session

model and fallbackModel both take either a bare string, which applies to everything, or a map naming a model per kind of session. The kinds are the ones :agents already shows:

triage, work, maintenance, coordinator, review, plan, draft-issue, draft-project and todo, plus general for everything you have not named.

{
  "model": {
    "general": "fable",
    "review": "opus",
    "maintenance": "haiku"
  }
}

A kind you do not name inherits general. A kind you name with an empty string is switched off: for model that means Claude Code picks, and for fallbackModel it means that kind never demotes. Switching a kind off is genuinely off, not inherited back from general.

"model": "fable" still means what it always did, which is { "general": "fable" }.

Per-task and per-dispatch overrides (m and c) outrank all of it for that task's work session. The fallback is resolved for the same kind as the model it accompanies, so a kind given its own model can be given its own way down as well.

Guidance

Standing house rules for agents — the things that are true of every PR you'd merge, so you don't retype them per issue. Either one block that reaches every agent:

"guidance": ["PRs should solve the problem at hand simply and clearly."]

or a map, where each scope's text is added to general for that one kind of work:

"guidance": {
  "general": ["applies to every agent"],
  "triage":  "scoping an issue",
  "work":    "implementing an issue",
  "review":  "reviewing someone else's PR",
  "plan":    "project planning chat"
}

Every value takes a string or a list of lines. Precedence: per-task instructions (m, or c at dispatch) outrank guidance, and repo-specific conventions still belong in that repo's CLAUDE.md, which agents read anyway.

Todo

What :todo ranks by. Every key is optional.

"todo": {
  "reviewSlaHours": 24,
  "staleDays": 14,
  "horizonDays": 21,
  "refreshMinutes": 0,
  "maxCandidates": 60,
  "guidance": "Mornings are for deep work: nothing below `today` before noon."
}
key default what
enabled true the whole feature: the view, the ranking agent and the refresh clock. "todo": false is the same as "todo": { "enabled": false }. Off, :todo explains how to turn it back on, and the daemon refuses to gather or rank
prompt the built-in policy replaces the policy the ranking agent is given, which says how to weigh reviews, deadlines and priority. String or list of lines
guidance none added after the policy, whether built in or replaced. It suits a rule or two you want on top of the defaults
reviewSlaHours 24 the review turnaround you hold yourself to, in working hours. A request past it goes to now
staleDays 14 a PR untouched this long is stale. Its clock stops and it sinks. A draft is always stale
horizonDays 21 how far ahead a deadline starts to count. Milestones and projects past it are not listed
refreshMinutes 0 re-rank on a clock, with the agent. 0 means only when you press r
maxCandidates 60 the most candidates handed to the agent, best-scored first. The rest are counted but not ranked

prompt replaces only the policy. The description of the data and the answer format stay the same, because they are what colinear parses. Without them a custom policy would stop the answer parsing, and the list would fall back to colinear's own order every time. To see the default policy, look for DEFAULT_TODO_POLICY in src/core/todo.ts.

The ranking agent is a session of kind todo, so "model": { "todo": "haiku" } gives it its own model. It is denied every tool that could write, whatever denyTools says.

Retention and disk

Two windows, deliberately different numbers — a checkout is exactly what you want the day a task lands, long after the card stops being interesting.

  • retentionDays (default 30, 0 keeps everything) — how long finished work stays on the board. Past it, done and cancelled tasks and settled reviews are forgotten; never anything with a live agent, a pending question, an open PR, or an error, however old. It's also the window the header's Tokens/30d ($…) figure covers, so the number and the board always agree.
  • worktreeRetentionDays (default 7) — how long a finished task's worktree survives before coli gc / :gc offer it for removal. Nothing is ever removed without you asking. --older-than N overrides it for one run.

Neither window has to be waited out: :gc lists finished cards beside reclaimable worktrees and forgets the ones you pick, using the same definition of "finished" the sweep does.

New sub-issues

Creating a sub-issue and spending an agent on it are separate statements, so by default they stay separate: A on a proposal or split plan creates the issues, D creates and dispatches, and u on the parent dispatches whatever is sitting there. A sub-issue you make in Linear yourself appears on the parent within a minute and waits.

autoDispatchSubs changes that for tracking parents: any sub-issue that colinear has no task for and that nobody has started in Linear gets dispatched on the next sweep. The m modal sets it per parent (config default / auto-dispatch / leave them), which is the useful granularity — one family running itself while the rest don't.

Three deliberate limits:

  • Linear state is the guard, not "do we have a task". A sub-issue that was worked months ago and later dropped from the board by retentionDays is started or completed in Linear, so it can never be resurrected by the sweep.
  • Five per sweep. Nobody is watching a 60-second timer, and a bulk import shouldn't assign twenty issues to you at once; the rest follow a minute later.
  • Auto-dispatched sub-issues get triaged. u and D skip triage because you looked at them first; these arrive with no human in the loop, so triage stays on to catch the too-big and under-specified ones.

Auto-rebase

When GitHub reports a PR conflicting with its base, colinear can dispatch a session that rebases it: resolve conflicts, run the linters and nearby tests, push --force-with-lease. autoRebase is the default (false); the m modal sets it per task (config default / auto-rebase / leave it), and b rebases on demand whatever the setting says.

One attempt per conflict, re-armed once the PR is mergeable again. A conflict GitHub hasn't finished computing (UNKNOWN) never triggers one. The card keeps its column and shows a blinking dot — green rebasing, amber fixing CI — since maintenance on an open PR isn't the feature being rewritten.

Issue providers

Colinear talks to an issue tracker through one interface (core/provider.ts) with a capability descriptor, rather than assuming Linear's model everywhere. :config names the provider, what it calls a scope (team / project / repo), and anything it can't do.

Capabilities exist because the trackers genuinely differ — GitHub Issues has no priority field, no "blocks" relation and only open/closed. Where one is missing the feature switches off rather than breaking: no workflowStates means stateSync never fires, no blockers means nothing parks as ⛓ blocked, no priority means the column disappears, no branchNames means colinear derives a safe branch from the identifier.

Linear supports all of it. sqlite is a local tracker in a file — issues, sub-issues, blocking relations, projects, states, priorities and comments, with no account and no network. It's how you try colinear in thirty seconds, it's what the demo board runs on, and it's the second implementation that keeps the interface honest. Its one gap is branchNames: there's no upstream to supply one, so colinear derives a safe branch from the identifier (LOC-2 → loc-2).

It needs node:sqlite — Node 24+, or Node 22 with --experimental-sqlite. The module is required lazily inside the provider, so nothing else in colinear depends on it.

File issues into it from the shell:

coli issue add "Add a rollback path"                  # LOC-1
coli issue add "Write the down migration" --parent LOC-1
coli issue add "Ship it" --priority 2 --desc "after the migration lands"

Because a context is already a config plus its own daemon and store, the natural way to run two trackers is one context each — which also keeps their issue ids from ever meeting in one store.

One requirement any adapter has to meet: identifier is load-bearing beyond display. It names branches, worktree directories and coordination channels, and PR matching looks for it in branch names and titles — so it has to be short, unique across the repos in play, and safe in a path and a git ref.

Experimental features

Features that work but aren't settled — the shape, the token cost or the prompt discipline may still change, and they can affect what agents do. Each needs two switches:

"experimental": true,
"experiments": { "coordination": true }

The master switch is separate so one line turns everything experimental off when something misbehaves, without you having to remember which features you'd enabled. Naming a feature without it — or naming something that isn't an experiment — is written to the debug log rather than silently ignored, so a feature never quietly fails to run.

experiment what
coordination Family coordination channels, and coordinator sessions for tracking parents. Sub-issue agents in one family share an IRC-style channel (#CLO-67) through in-process MCP tools: channel_read (only what's new since that agent last read) and channel_post (identity stamped at spawn — an agent can't pose as a sibling or reach another family's channel). They're prompted to claim scopes, announce architectural decisions, flag shared resources they're using, and read before opening a PR. :chan lists channels, :chan CLO-67 tails one with an input box — your message reaches every agent in that family at its next read. Issues in a Linear project also share a project channel (#proj-cloud-migration) — the agents you'd otherwise never hear from, different families on the same release — and M in :project NAME broadcasts to it. The read/post tools take a scope whose options are exactly the channels that session is in, so an agent can pick between its family and its project but can't reach a family it isn't part of. A tracking parent — an issue whose work happens in its sub-issues — becomes coordinatable: M a message to it (or r) starts a coordinator session that can read the family's live state, relay instructions to a sibling's agent, cancel one, and propose new sub-issues. It writes no code and gets no checkout. It also cannot create Linear issues: proposals land on the parent card and wait for your A. Full design, storage layout and deferred work: COORDINATION.md

Turning one on changes the daemon's behavior, so it needs coli daemon stop && coli, not just R.

Contexts

A context is one config file plus its own state — separate daemon, socket, task store and log. Use one per Linear workspace, team, or machine role; two can run side by side without either seeing the other's tasks.

# ~/.config/colinear/contexts/oss.json
coli --context oss          # or -c oss, or COLINEAR_CONTEXT=oss coli
coli contexts               # what exists, where, and which have a daemon up
{
  "team": "OSS",
  "concurrency": 1,
  "repos": [{ "name": "colinear", "path": "~/work/colinear" }]
}

A context layers over the default config: keys it doesn't set are inherited, so shared settings (key, guidance, signoff) get written once. Top-level keys replace wholesale rather than merging — a context that sets repos gets exactly those repos.

Everything downstream follows the context: the daemon it starts or attaches to, state.json, the debug log, coli gc, and :config's e. The board's header shows (ctx oss) next to the repo whenever you aren't in the default one, and coli daemon status names it — "no daemon running" is otherwise confusing when the real reason is that you're pointed at a different context. Naming a context with no config file is an error, not a quiet fallback to the default: dispatching into the wrong workspace is not a recoverable mistake.

Where the config lives

~/.config/colinear/config.json, or ~/.colinear.json. A context reads ~/.config/colinear/contexts/<name>.json layered over it.

coli init writes the first one. :config shows the resolved settings with the key masked, names the provider and its capabilities, and e opens the file in $EDITOR — changes apply when you come back. A file that exists but doesn't parse is a startup error, not a silent fall back to defaults.

Custom views

Drop JSON in ~/.config/colinear/views/, then :reload:

{
  "name": "cloud-bugs",
  "aliases": ["cb"],
  "describe": "unowned CLOUD bugs",
  "filter": { "team": "CLOUD", "labels": ["Bug"], "assignee": "any", "project": null },
  "columns": ["issue", "priority", "title", "labels"],
  "sort": "priority"
}

filter: team, labels[], state[] (workflow types), assignee (me/any), project (null = no project, or a name).

Files on disk

what where
config ~/.config/colinear/config.json
contexts ~/.config/colinear/contexts/<name>.json
custom views ~/.config/colinear/views/*.json
task / planner / UI state ~/.local/state/colinear/state.json (pruned by retentionDays)
debug log + diverted stderr ~/.local/state/colinear/colinear.log
daemon socket + pidfile ~/.local/state/colinear/coli.sock, coli.pid
worktrees <repo>-worktrees/<ISSUE-KEY>, review checkouts review-<n>
session transcripts ~/.claude/projects/<encoded-worktree>/<session>.jsonl (Claude Code's own)

A non-default context moves all the state paths under ~/.local/state/colinear/contexts/<name>/; COLINEAR_STATE_DIR overrides them outright.