Skip to content

Configuration

The configuration is $XDG_CONFIG_HOME/wloop/config.toml, by default ~/.config/wloop/config.toml, a TOML file like the profile’s manifests; # starts a comment. Every key is optional; a key the loop does not read is an error that names it, and a close key when one exists, and wloop doctor reports it. A value of the wrong kind, or outside its set, is refused the same way: a duration or a date written without quotes, a number where a string goes, inf or nan. A key left out keeps its default. A former config.json is not read; when it is the only file, every command that loads the configuration fails, naming config.toml. wloop config get <key> prints the effective value of one, defaults included (sweep.fast for a nested key). wloop config set <key> <value> writes one key, keeping the file’s comments and layout, and wloop config unset <key> removes one, or a whole table (workflows.<name>); a table an unset leaves with no key, comment or sub-table loses its header too, so an unset undoes a set. Set refuses a key no setting reads, a workflows.<name> key whose name is none of the profile’s workflows (wloop doctor lists them) nor a disabled plugin’s, which can be switched before the plugin is enabled, and a result the loop would refuse; unset removes any key, one no setting reads too, and refuses only a result that would no longer load. Each keeps the previous file as config.toml.loop-<stamp>.bak.

The daemon reads the file when it starts. It follows a change of profile, ui.locale and workflows.<name>.enabled live, the keys its stale warning lists; any other change takes effect after a restart of the daemon, and until then the daemon reports its configuration as stale. The window keys are the app’s: a change applies when the app restarts, and the daemon does not report it. Durations are strings: "2d", "36h", "90m" or "60s". wloop profile schema --config prints the file’s JSON Schema, for an editor whose TOML support reads one.

A plugin’s settings live under its id (github, slack), and <id>.enabled switches it, read at start. workflows.<name> overlays a workflow’s manifest field by field, as Tune workflows shows; its gates.approval takes mode, window and rule, a list of tables with when, mode and window ([[workflows.<name>.gates.approval.rule]]), for every gate the workflow opens, a rule for the kinds its when pins gate.kind to: a rule list replaces the manifest’s rules and [] clears them, a key left out keeps the manifest’s, and a block that does not merge with the manifest’s is named by doctor. intents.<name>.project_settings.when is a predicate over the turn facts of the session’s unit, such as a pull request’s, where the intent’s sessions load the checkout’s own agent settings (.claude/, .mcp.json, CLAUDE.md); elsewhere they start with your settings alone, and an intent it does not name loads them; see Review. Unless the table says otherwise, workspace_managers is ["claude-desktop", "orca"] and agent is "claude".

SettingKindDefaultMeaning
profilestringThe user profile layered over the bundled one: a name under the profiles directory, or a path; an intent’s profile names an intent.
agentstringThe agent the sessions run.
agent_homestringThe agent’s own configuration directory; empty is the agent’s default.
agent_modelstable by namePer agent, a model name for each model tier.
agent_models.&lt;name&gt;table by nameOne agent’s models by tier.
agent_models.&lt;name&gt;.&lt;name&gt;stringThe agent’s model name for the tier.
projects_dirstringWhere review clones a repository the workspace manager does not know yet.
workspace_managerslist of stringsThe enabled workspace managers, in the order a new worktree picks them.
intentstable by nameEach intent’s settings, by intent name.
intents.&lt;name&gt;tableOne intent’s settings.
intents.&lt;name&gt;.project_settingstableWhere the intent’s sessions load the checkout’s own agent settings.
intents.&lt;name&gt;.project_settings.whenpredicateWhere the intent’s sessions load the checkout’s settings; elsewhere they start without them.
SettingKindDefaultMeaning
projects[]tableSettings for the pull requests and the work of matching repositories; the first match wins.
projects[].matchstringAn owner/repo pattern, such as acme/* or acme/api.
projects[].projects_dirstringThe project’s projects_dir.
projects[].agentstringThe agent the project’s sessions run.
projects[].agent_homestringThe project’s agent_home.
projects[].workspace_managerslist of stringsThe managers a new worktree of the project picks from, in order.
projects[].trust_clonesboolTrusts a repository of the project in the agent home at the moment the loop clones it, so its settings and hooks run as you; the match must name its owner, such as acme/*.
projects[].githubtableThe GitHub settings the project overrides.
projects[].github.trusted_authorslist of stringsThe project’s plugins.github.trusted_authors.
projects[].github.followups_without_workspaceboolThe project’s plugins.github.followups_without_workspace.
projects[].github.address_without_workspaceboolThe project’s plugins.github.address_without_workspace.
SettingKindDefaultMeaning
placement[]tableThe rules that place units; the first match wins.
placement[].matchstringThe units the rule places.
placement[].repostringThe repository their work checks out.
placement[].checkoutstringThe branch their work checks out.
placement[].roleone of review, author, workThe role of their workspace.
placement[].sharedboolThe units share one workspace.
SettingKindDefaultMeaning
pluginstableEach plugin’s table, by id: its switch, its trust and its settings.
plugins.&lt;name&gt;tableA plugin’s table; the keys besides enabled and trust are the plugin’s own, which only it reads.
plugins.&lt;name&gt;.enabledboolSwitches the plugin; left out, it is on.
plugins.&lt;name&gt;.trustone of pathpath runs the plugin’s binary whatever its checksum; left out, a bundled plugin runs only with the checksum its install recorded, and any other plugin does not run. Only you write it.
plugins.&lt;name&gt;.&lt;name&gt;string or listA key of the plugin’s own.
SettingKindDefaultMeaning
plugins.githubtableThe GitHub plugin’s settings.
plugins.github.enabledboolSwitches the GitHub plugin; left out, it is on.
plugins.github.trustone of pathpath runs the GitHub plugin’s binary whatever its checksum; left out, it runs only with the checksum its install recorded.
plugins.github.review_prefixstringREVIEW The prefix of a review workspace’s name.
plugins.github.automated_reviewerslist of stringsLogins of review bots whose reviews count as automated.
plugins.github.ownerslist of stringsThe GitHub owners automated follow-ups run on; empty is every owner the workspace manager knows.
plugins.github.trusted_authorslist of stringsLogins whose pull requests pass the author association check of automated rounds.
plugins.github.followups_without_workspaceboolfalseA follow-up review recreates the review workspace the user deleted; off, the sweep only logs it.
plugins.github.address_without_workspaceboolfalseAn address round creates its author workspace when it has none; off, the sweep only records it.
plugins.github.rate_limit_fractionnumber0.5The share of the GitHub rate limit the sweeps may spend, a number between 0 and 1.
plugins.github.gatestableThe gates the plugin’s own rules open.
plugins.github.gates.approvaltableTheir approval; left out, the default gates.toml gives a draft gate.
plugins.github.gates.approval.modeone of wait, delay, send, dry_run, runThe approval mode.
plugins.github.gates.approval.windowdurationHow long a delay waits before the gate’s consequence; only with mode delay.
SettingKindDefaultMeaning
plugins.github.address_reviewsbooltrueSomeone’s review of a pull request you develop starts an address round.
plugins.github.address_skip_reviewerslist of stringsLogins whose reviews never start an address round.
plugins.github.address_checksbooltrueFailing checks on the head of a pull request you develop start an address round.
plugins.github.ignore_checkslist of stringsChecks whose failure never starts an address round.
plugins.github.address_conflictsbooltrueA pull request you develop that conflicts with its base past the conflict window starts an address round.
SettingKindDefaultMeaning
plugins.github.scopetableWhose pull requests the GitHub plugin follows.
plugins.github.scope.peoplelist of stringsLogins whose pull requests it follows.
plugins.github.scope.teamslist of stringsTeams whose pull requests it follows.
plugins.github.scope.reposlist of stringsRepositories whose pull requests it follows.
plugins.github.scope.watchlist of stringsRepositories it watches.
plugins.github.stalledtableWhen a pull request counts as stalled.
plugins.github.stalled.reviewduration2dWaiting on a review for this long.
plugins.github.stalled.replyduration1dWaiting on a reply for this long.
plugins.github.stalled.mergeduration1dApproved and unmerged for this long.
plugins.github.nudgetableThe nudges on pull requests.
plugins.github.nudge.silent_daysinteger30Calendar days of silence on a pull request before a nudge is due; workflows read it as config.plugins.github.nudge.silent_days.
SettingKindDefaultMeaning
plugins.slacktableThe Slack plugin’s settings; no secret lives here.
plugins.slack.enabledboolSwitches the Slack plugin; left out, it is on.
plugins.slack.trustone of pathpath runs the Slack plugin’s binary whatever its checksum; left out, it runs only with the checksum its install recorded.
plugins.slack.client_idstringThe Slack app’s client id; empty leaves Slack unconfigured.
plugins.slack.redirect_portintegerThe login callback port registered in the Slack app; 0 is 53682.
plugins.slack.keychain_servicestringThe name of the Keychain items; empty is the brand’s.
plugins.slack.post_allowlistlist of stringsWhen not empty, the only channel ids, or user ids naming the DM with that user, a post may go to.
plugins.slack.request_logboolfalseLogs one line per Slack call: method and HTTP status.
plugins.slack.ended_afterduration7dHow long a Slack unit stays quiet, its newest stored message that old, before the plugin says it has ended (slack.ended).
plugins.slack.turnstableWhat a turn on a Slack unit reads.
plugins.slack.turns.max_charsintegerThe characters of the unit’s messages rendered into the instructions; 0 is 30000.
plugins.slack.gatestableThe gates the plugin’s own rules open.
plugins.slack.gates.approvaltableTheir approval; left out, the default gates.toml gives a draft gate.
plugins.slack.gates.approval.modeone of wait, delay, send, dry_run, runThe approval mode.
plugins.slack.gates.approval.windowdurationHow long a delay waits before the gate’s consequence; only with mode delay.
SettingKindDefaultMeaning
plugins.slack.ingesttableWhat the Slack plugin admits and how it listens.
plugins.slack.ingest.termslist of stringsA message that holds one of them as a word, in any case, is admitted.
plugins.slack.ingest.allow_appslist of stringsBot ids (B…) or app ids (A…) whose messages are admitted.
plugins.slack.ingest.deny_appslist of stringsBot ids (B…) or app ids (A…) whose messages are not.
plugins.slack.ingest.socketboolListens on a socket; left out, it does.
plugins.slack.ingest.sincestringThe first start’s backfill date, YYYY-MM-DD; empty is 7 days back.
SettingKindDefaultMeaning
plugins.slack.extractiontableThe extraction job; a zero value keeps its default.
plugins.slack.extraction.enabledboolSwitches the extraction job; left out, it is on.
plugins.slack.extraction.tierone of fast, balanced, frontierIts model tier; left out, fast.
plugins.slack.extraction.languagestringThe language the extraction writes in; empty is English.
plugins.slack.extraction.per_call_usdnumberWhat one extraction call may cost, in USD; 0 is 0.10.
plugins.slack.extraction.per_day_usdnumberWhat extraction may cost in a day, in USD; 0 is 3.
plugins.slack.extraction.per_dayintegerExtraction calls per day; 0 is 500.
plugins.slack.extraction.quietdurationHow long a unit stays unchanged before it runs; empty is 90s.
plugins.slack.extraction.max_waitdurationThe longest delay of a unit’s oldest change; empty is 10m.
plugins.slack.extraction.window_charsintegerThe characters of conversation one call reads; 0 is 40000.
plugins.slack.extraction.max_failuresintegerFailed calls in a row after which the job gives up on a unit until its next change; 0 is 3.
plugins.slack.extraction.context_messagesintegerThe read-only messages shown before a DM’s window; 0 is 10, -1 turns them off.
plugins.slack.extraction.context_charsintegerThe characters of those messages; 0 is 4000, -1 turns them off.
plugins.slack.extraction.escalate_over_charsintegerA unit whose rendered conversation is longer goes to the frontier tier; 0 or -1 is off.
SettingKindDefaultMeaning
workflowstable by nameOverrides of each workflow’s manifest, by workflow name.
workflows.&lt;name&gt;tableOne workflow’s overrides.
workflows.&lt;name&gt;.enabledboolSwitches the workflow; left out, it is on. Off, it proposes no draft, and the drafts it holds stay.
workflows.&lt;name&gt;.observeboolPlans the workflow’s items pending, for you to run; left out, the manifest’s.
workflows.&lt;name&gt;.only_whenpredicateA condition ANDed with the workflow’s when.
SettingKindDefaultMeaning
workflows.&lt;name&gt;.gatestableThe gates the workflow opens: its runs’ plan gates, its drafts’, its turns’ drafts’ and its gate.open steps’.
workflows.&lt;name&gt;.gates.approvaltableThe approval of the workflow’s gates, a rule for the kinds its when pins gate.kind to, over the manifest’s field by field.
workflows.&lt;name&gt;.gates.approval.modeone of wait, delay, send, dry_run, runThe mode when no rule matches; left out, the manifest’s.
workflows.&lt;name&gt;.gates.approval.windowdurationHow long a delay waits before it goes; only with mode delay.
workflows.&lt;name&gt;.gates.approval.rule[]tableRules tried in order before the mode, the first match wins; set, they replace the manifest’s, and [] clears them.
workflows.&lt;name&gt;.gates.approval.rule[].whenpredicateWhen the rule applies.
workflows.&lt;name&gt;.gates.approval.rule[].modeone of wait, delay, send, dry_run, runThe rule’s mode.
workflows.&lt;name&gt;.gates.approval.rule[].windowdurationThe rule’s delay; only with mode delay.
SettingKindDefaultMeaning
turnstableThe automated turn budgets.
turns.per_dayinteger20Automated turns per day, all workflows together, on units whose plugin keeps no per-unit count (every provider but GitHub today).
turns.per_unit_per_dayinteger5Automated turns on one unit per local day, on units whose plugin keeps a per-unit count (GitHub today); every automated turn spends one, whatever its intent declares, and a turn by hand none.
headlesstableThe headless turns’ caps; a cost of zero leaves it uncapped.
headless.tierone of fast, balanced, frontierbalancedThe model tier of their calls.
headless.per_call_usdnumber0.5What one call may cost, in USD.
headless.per_day_usdnumber5What they may cost in a day, in USD.
brieftableThe brief: one headless call that previews each unit.
brief.enabledboolfalseBriefs each unit.
brief.tierone of fast, balanced, frontierfastThe brief’s model tier.
brief.max_input_bytesinteger60000The most bytes of input a brief reads, between 1000 and 200000.
classifytableThe classify step.
classify.per_dayinteger50Classify calls per day.
SettingKindDefaultMeaning
draftstableThe settings every proposer’s drafts share.
drafts.repeat_afterduration7dThe default repeat_after a config.drafts.repeat_after reference reads, and the plugin rules’ delay before they propose a settled draft’s key again.
notificationstableThe notifications.
notifications.digest_atstring09:00The local HH:MM of the daily digest; empty for none.
SettingKindDefaultMeaning
windowtableThe app’s own window on macOS, which the app reads when it starts.
window.enabledbooltrueShow the cockpit in the app’s own window on macOS; false opens it in the browser.
window.hotkeystringctrl+option+spaceThe global shortcut that opens the omnibox panel, such as cmd+shift+space; empty for none.
SettingKindDefaultMeaning
itemstableThe items.
items.heattableHow long a unit stays warm.
items.heat.warm_fordurationHow long a unit stays warm after its last item resolved; left out, the heat reader’s default.
taskstableThe tasks’ settings.
tasks.stale_waiting_daysinteger2Working days after which a waiting task goes stale.
tasks.areaslist of stringsThe areas a task is offered.
tasks.contextslist of stringsThe contexts a task is offered.
tasks.candidate_windowduration14dHow far apart two tasks’ creations may be for task.candidates to pair them.
tasks.candidate_capinteger5The most tasks task.candidates lists; 0 lists none, so no correlation runs.
SettingKindDefaultMeaning
cleanuptableThe thresholds of the session and workspace cleanup workflows.
cleanup.idleduration2hHow long a finished session stays idle before it closes.
cleanup.asked_idleduration4hHow long a session that asked you stays idle before it closes.
cleanup.workspace_graceduration1dHow long, in working time, a workspace stays after its unit finished.
cleanup.manual_idleduration1dHow long a session you started stays idle before it closes.
cleanup.stale_workspaceduration14dHow long an untouched workspace stays.
cleanup.orphan_holdduration1dHow long the close of a dormant session waits while its process lingers orphaned, the app showing some terminal throughout, before it closes the session’s record anyway; it ends no process.
cleanup.disposablelist of stringsIgnored files and directories a tool recreates, so removing a workspace that holds them loses nothing.
SettingKindDefaultMeaning
dossierstableThe dossiers’ lifecycle and quotas; a value of zero or less keeps its default.
dossiers.archive_afterduration30dHow long after its task or unit resolved a dossier is archived.
dossiers.purge_afterduration400dHow long after the archive it is purged.
dossiers.soft_cap_mbinteger50The size per dossier doctor reports.
dossiers.total_cap_mbinteger2048The size of dossiers and archives doctor reports.
dossiers.max_file_mbinteger5The largest file a dossier keeps; a larger one is linked.
dossiers.compact_at_kbinteger256The log size that triggers compaction.
SettingKindDefaultMeaning
uitableThe interface.
ui.localestringThe interface language, a BCP 47 tag such as pt-BR; empty follows the system’s preferred languages. Applies without a restart.
SettingKindDefaultMeaning
sweeptableHow often the sweeps run.
sweep.fastduration2mThe fast sweep’s interval.
sweep.slowduration1hThe slow sweep’s interval.
httptableThe daemon’s web cockpit.
http.portinteger0The cockpit’s port; 0 is none.
mcptableThe daemon’s MCP endpoint, which agent homes register.
mcp.portinteger47800The MCP endpoint’s port.
viewstable by nameEach view sink’s own settings, by sink.
views.&lt;name&gt;tableOne sink’s settings.
views.&lt;name&gt;.columnstable by nameThe sink’s column for each status: working, working-fallback, needs-me, reviewed, ready, in-progress, in-review, completed.
views.&lt;name&gt;.columns.&lt;name&gt;stringThe column of the status.
[[projects]]
match = "me/*"
agent_home = "~/.claude-personal"
projects_dir = "~/projects/oss"
[[projects]]
match = "acme/api"
github.address_without_workspace = true

match is owner/repo or a glob such as owner/*, case-insensitive; the first match wins. An entry may set projects_dir, workspace_managers (replaces the top-level list), agent, agent_home, and under github the project’s trusted_authors (replaces the top-level list), followups_without_workspace and address_without_workspace. Empty fields keep the top-level value. What your profile says for those repositories goes in its organization= and repo= blocks.

workspace_managers = ["claude-desktop", "orca"]
[[projects]]
match = "acme/*"
workspace_managers = ["orca"] # replaces the top-level list

wloop serves every listed manager at once: it reads their worktrees and sessions and acts on each worktree through its owner. The order matters only for a new worktree, which goes to the first manager that is

  • enabled: listed here, or in the matching projects entry;
  • available: installed and reachable;
  • able to host the project’s agent;
  • able to honor the session’s launch policy.

Orca honors every launch policy. Claude Desktop honors a session that loads the checkout’s project settings, uses the default agent home, and adds allows and denies, which wloop writes into the worktree’s .claude/settings.local.json. A session that needs your user settings alone (where intents.<name>.project_settings.when does not hold for its intent, and always for a pull request that fails the trust check, whose checkout settings are its author’s), a strict or MCP-free session, or a non-default agent_home falls through to the next manager. When no manager can launch it, the round fails closed and names each manager’s reason. wloop doctor reports whether each manager is available and reachable (one unreachable manager is a note, all of them unreachable is a failure) and a launch <intent> check for each intent no enabled manager can launch.

An empty list is an error. The old workspace_manager key is refused; list the manager in workspace_managers instead.

Only Orca has a board. A workspace Claude Desktop owns has no card; its state shows in the cockpit, the terminal client and the menu bar.

views.orca.columns maps a status to a column: working (while a review round runs, reviewing by default), working-fallback (when the board has no working column, in-review), needs-me (an agent asked or stopped), reviewed (a review published without approving), ready (your approved, green pull request, ready-to-merge), and the author statuses in-progress, in-review and completed. See board columns.

[[placement]]
match = "slack:T000/C000/*"
repo = "acme/api"
checkout = "main"
role = "work"

The first rule whose match, a glob over <provider>:<key>, matches gives the repository, the checkout ("" for a new worktree, main, or a path), the role and whether the workspace is shared by several units. A unit no rule, hint or provider default places, such as a Slack DM, runs its sessions in <state>/neutral, an empty repository with no project checked out.

projects_dir = "~/projects"
profile = "mine"
[plugins.github]
trusted_authors = ["github-actions[bot]"]
[plugins.github.scope]
teams = ["acme/backend"]
repos = ["acme/api"]
[brief]
enabled = true
# cleanup removes these workspaces once their plans read right
[workflows]
workspace-review-finished.observe = false
workspace-author-finished.observe = false
[[projects]]
match = "me/*"
agent_home = "~/.claude-personal"