Skip to content

Configuration

Systematic works out of the box with sensible defaults. This page covers every way to customize its behavior.

Systematic looks for configuration in three places, checked in this order (highest priority first):

  1. Custom config directory: $OPENCODE_CONFIG_DIR/systematic.{json,jsonc}
  2. Project-specific: .opencode/systematic.{json,jsonc}
  3. Global: ~/.config/opencode/systematic.{json,jsonc}

When both .jsonc and .json exist in the same directory, .jsonc takes precedence and .json is silently ignored. This lets you add comments to your config without renaming the existing file.

{
"disabled_skills": ["git-worktree"],
"disabled_agents": []
}
{
"categories": {
// Applies to bundled agents in agents/review/*.md unless that agent has
// been replaced by a native OpenCode agent with the same emitted key.
"review": {
"temperature": 0.1,
"steps": 12
}
}
}
{
"agents": {
"security-reviewer": {
// User or custom config only; project config cannot choose model/provider routing.
"model": "anthropic/claude-sonnet-5",
"variant": "thinking",
"temperature": 0.2,
"steps": 30
}
}
}

Fields that control provider routing or capability policy — model, variant, permission, and skills — are accepted only from user config (~/.config/opencode/systematic.json) or $OPENCODE_CONFIG_DIR/systematic.json. Project config may tune non-sensitive fields such as temperature, top_p, mode, color, steps, hidden, or exact-agent disable, but it cannot choose model/provider routing or loosen permission/capability policy.

A typo in agents.<key>, disabled_agents, or disabled_skills fails loudly with a clear error that names the offending key — no silent misconfiguration.

The workflow guard records privacy-safe receipts for supported engineering operations and surfaces whether guarded workflow requirements are satisfied. It covers local implementation and verification work, commits, pushes, and GitHub pull-request, check, and review readback when the required Git and GitHub environment is available.

The guard ships in observe mode by default. It records and reports workflow state without blocking operations. Protected mode is opt-in; it can reject a guarded transition when required evidence is missing or stale. Disabled mode turns the guard off for the configured scope.

{
"workflow_guard": {
"mode": "observe",
"debug": false
}
}

workflow_guard is trust-protected. Set it in user configuration (~/.config/opencode/systematic.json) or $OPENCODE_CONFIG_DIR/systematic.json; project-level .opencode/systematic.json cannot set this field.

Use top-level agents and categories maps to tune bundled Systematic agents without copying their markdown files into native OpenCode agent definitions.

Overlay application precedence for a Systematic-owned bundled agent (strongest first):

  1. Exact agents.<key> overlay (high-trust model wins)
  2. categories.<category-id> overlay (high-trust model wins)
  3. Bundled markdown/frontmatter defaults (model-free)
  4. OpenCode inherited defaults

Bundled agents inherit the invoking model unless high-trust user or $OPENCODE_CONFIG_DIR/systematic.json config supplies an explicit model. Set "model": null to clear an explicit model and restore parent-model inheritance. Project config cannot set, erase, or shadow model at any value.

A flat agents.<name>.model now also routes in-process Pi delegates, not just OpenCode — previously it applied to OpenCode only.

For a task-oriented walkthrough, see the model profiles guide. A profile is a named, reusable bundle of routing overrides — model, variant, temperature, top_p, and the opencode/pi blocks below — defined once in your user config’s profiles map and selected by name. Profiles exist for people who work across repositories that warrant different model routing from the same harness: define work and personal profiles once in user config, then let each repository pick one.

// ~/.config/opencode/systematic.json (or $OPENCODE_CONFIG_DIR/systematic.json)
{
"profile": "personal", // your default when nothing else selects one
"profiles": {
"work": {
"agents": { "correctness-reviewer": { "model": "openai/gpt-6-astra" } }
},
"personal": {
"categories": { "review": { "model": "anthropic/claude-opus-5" } }
}
}
}

Any config source may set the top-level profile selector to choose among the user’s profiles — including a repository’s project config:

// .opencode/systematic.json (project config)
{ "profile": "work" }

Only profile (the selector) is project-settable by default. profiles (the map of bundle definitions) is trust-protected exactly like model, variant, permission, and skills — a project may choose among the profiles you’ve defined, but it cannot define new ones or see into ones you haven’t shared, unless you opt in (see below). A profiles map set in project config is ignored with a warning unless the opt-in is on. Both your user config and $OPENCODE_CONFIG_DIR config (“custom” config) may define a profiles map; a name is looked up in custom’s map first, then user’s, so a bundle defined only in custom config is still selectable by any source.

Opting a repository into defining its own profiles

Section titled “Opting a repository into defining its own profiles”

Setting allow_project_profiles: true in your user (or $OPENCODE_CONFIG_DIR) config lets a project systematic.json define its own profiles map and have it consulted for bundle content — useful when a repository wants to ship routing recommendations (e.g. “use this cheaper model for this repo’s review agents”) without you hand-copying them into your own config.

~/.config/opencode/systematic.json
{ "allow_project_profiles": true }
// .opencode/systematic.json (project config, once you've opted in)
{
"profile": "ci-cheap",
"profiles": {
"ci-cheap": {
"agents": { "correctness-reviewer": { "model": "anthropic/claude-haiku" } }
}
}
}

allow_project_profiles is user-owned only — exactly like profiles itself and workflow_guard, a project config can never set this field; doing so is stripped before the project config is even parsed and produces the same “ignored” warning as any other project-trust field. A project cannot grant itself a permission it does not already have. The opt-in is resolved from your user and $OPENCODE_CONFIG_DIR config only, before the project config is loaded, so setting it in the SAME file as a profiles map does not work either — both are stripped together, as if neither had been written.

A project-defined bundle is advisory, not authoritative: it supplies routing only where your own config is silent for that field, and can never override a value you set — not even partially. If you set agents.correctness-reviewer.model yourself, a project bundle that also sets it has no effect on that agent’s model at all; the project’s value never applies and never appears anywhere in the resolved config. This is enforced structurally by where the project bundle sits in the merge chain (before your base config, not after) combined with field-by-field merging — not by a policy check that a future change could accidentally bypass. A repository whose entire suggested profile is already covered by your own config is a normal, silent no-op: systematic config show shows you exactly which fields, if any, actually came from the project (see the model profiles guide).

A project-defined profile name can never shadow one you or $OPENCODE_CONFIG_DIR config already defined: bundle lookup checks custom, then user, then project last. If your personal profile and a repository’s personal profile both exist, yours always wins the name. This protects a name you have defined content for — it does not protect a name you have only referenced. If your own profile default names a bundle you never defined (or typoed), that name is still looked up through the same custom-then-user-then-project order, so an opted-in repository’s own profiles entry of that same name is consulted and can supply content for it; the usual “not defined in profiles” warning does not fire, because the name IS defined — by the repository, not by you.

Because assertAllProfileBundlesAreValid validates every bundle in every source that may define one — including, once opted in, every bundle in a project’s profiles map — a malformed bundle anywhere in that map can fail the entire config load (and therefore plugin init) even if nobody ever selects it. This applies to a bundle you clone from a repository just as much as one you write yourself: opting in makes your config load depend on content you did not author.

The repository still decides which of its own (or your) profiles is active for itself, via its project config’s profile selector — same as today, opt-in or not. That is the point of a project selecting a profile, not a gap: a repository choosing to route itself through the work profile you already defined is exactly what profile selection is for. If you disagree with a repository’s selection for your own session, SYSTEMATIC_PROFILE=<name> in your shell environment outranks every config source’s profile value, including the project’s — set it to override, or to a nonexistent name to fall back to your own default.

profile is tri-state: leaving it unset means “no opinion, let a lower-priority source decide”; setting it to a name selects that bundle; setting it to null explicitly means “base configuration, no profile” and wins outright over a weaker source’s name — this is how a repository can opt out of your default profile rather than merely failing to set one. The strongest source that sets profile at all wins: custom config, then project config, then user config.

If the selected name doesn’t match a defined profile (in either custom’s or user’s profiles map), Systematic warns once and falls back to your own profile default (if you have one and it resolves — checked the same way, custom first, then user); if that’s also undefined, it falls back to base configuration with no profile active. Run systematic config show to see the active profile, which source selected it, and any fallback that occurred.

Switching which profile is active never changes an agent’s visibility, permissions, mode, or existence: unlike an ordinary same-key overlay override (which fully replaces the previous value), the active profile’s overlay merges field-by-field over the base config — a field the profile bundle is schema-incapable of carrying (mode, color, steps, hidden, disable, permission, skills) simply survives untouched regardless of which profile is selected. ProfileOverlaySchema restricts a profile entry to routing-only fields for exactly this reason.

agents.<name> and categories.<category> overlays accept optional opencode and pi blocks so one config routes correctly on both harnesses, while the existing flat model/variant fields stay as the harness-neutral default:

{
"agents": {
"correctness-reviewer": {
"model": "anthropic/claude-sonnet-5", // harness-neutral default
"opencode": { "variant": "thinking" }, // OpenCode-only qualifier
"pi": { "thinking": "high" } // Pi-only qualifier
}
}
}

Resolution precedence, evaluated independently for model and for the harness’s qualifier (variant on OpenCode, thinking on Pi): agent block > agent flat > category block > category flat. Merge order across sources is user base → selected profile → project → custom, with each later source overriding the same field at the same level — when the active profile came from your user or $OPENCODE_CONFIG_DIR config. When the active profile is a project-defined bundle (the opt-in above), it merges before user base instead, so your base config overrides it field by field rather than the other way around; see the opt-in section above for what that means in practice.

model: null is a value, not an absence — it means “inherit from the parent session” and beats a lower layer’s explicit model at the same precedence level. But it obeys the same precedence as any other value: a block’s null beats a flat explicit model at the same level, not the other way around. Concretely, a profile that sets agents.x.model: null does not undo a base config’s agents.x.opencode.model — the block is more specific than the flat field it’s trying to override, so OpenCode keeps the block’s model. To restore inheritance on OpenCode specifically, set agents.x.opencode.model: null. When model: null wins at layer L on OpenCode, any variant is dropped too — inheriting the parent’s model means inheriting its variant as well; there is no “inherited model, explicit variant” combination.

The two harnesses’ qualifiers bind to model differently. OpenCode’s variant is coupled to whichever layer supplies model: let L be the most specific layer (agent block > agent flat > category block > category flat) that sets model. variant is used only from L or a MORE specific layer than L — a variant from a LESS specific layer is dropped, so a more specific layer’s model change doesn’t accidentally inherit a stale variant from further up the merge chain:

{
"categories": {
"review": { "model": "anthropic/claude-sonnet-5", "variant": "high" }
},
"agents": {
// Overrides the category's model. The category's "high" variant does NOT
// carry over -- it was set at a less specific layer than the agent's
// model override, so it's dropped, not inherited.
"correctness-reviewer": { "model": "openai/gpt-6-astra" }
}
}

Setting variant with no model resolving anywhere for that agent on OpenCode — after every layer is merged — is a config-load error naming the agent and opencode. A profile is allowed to set opencode.variant alone precisely because the model is expected to come from a lower layer (your base config); the error only fires when no layer supplies one on OpenCode.

Pi’s thinking qualifier, by contrast, is independent of model: it applies to whatever model the Pi delegate ends up running, including a model inherited from the parent session when no layer sets one at all. pi.thinking with no model resolving anywhere is a normal, valid configuration — never a config-load error. This mirrors how the deprecated pi_subagents.thinking field always behaved.

pi_subagents.<name>.thinking is deprecated

Section titled “pi_subagents.<name>.thinking is deprecated”

The legacy pi_subagents.<name>.thinking field (and its category form) is superseded by agents.<name>.pi.thinking / categories.<category>.pi.thinking. The new location wins when both are set; the legacy field is honored only when the new location is unset, and using it prints one deprecation warning naming the exact legacy field you wrote — a category-level pi_subagents.categories.<category>.thinking write prints one warning naming that category, not one per agent in it. Migrate by moving thinking into the pi block.

Add a $schema field to your systematic.json to enable per-key autocomplete in editors that honor JSON Schema property completion:

{
"$schema": "https://fro.bot/systematic/schemas/v3/systematic-config.schema.json",
"disabled_skills": ["git-worktree"],
"agents": {
"correctness-reviewer": { "temperature": 0.1 },
"security-reviewer": { "steps": 20 }
}
}

VS Code is the reference target — it picks up $schema automatically and provides inline completion, hover documentation, and validation squiggles. Zed, IntelliJ, and other editors that support JSON Schema property completion will also benefit, though support varies by editor version.

Since v2.15.0, Systematic validates agents.<key>, disabled_agents, and disabled_skills against the actual set of bundled names at load time. A typo fails loudly with a clear error that names the offending key. The valid bundled names are listed in the error when validation fails, so the error message is the fix.

If your config contains a typo in any of these fields, Systematic will fail to load on upgrade and surface the error. The fix is to correct the key to a valid bundled name.


The sections below are auto-generated from src/lib/config-schema.ts. Do NOT edit the content between the SYSTEMATIC:FIELD-REFERENCE markers — run bun run docs:generate to regenerate.

The agents and categories sections share the same set of per-entry overlay fields. Each key under agents or categories is an object with the following fields. (The disable field is only valid under agents, not categories.)

Model identifier in provider/model format, or null to inherit parent model

Type: string | null

Examples:

anthropic/claude-sonnet-5
null

Model variant identifier

Type: string

Examples:

v2
extended

Sampling temperature (≥0; 0 = deterministic)

Type: number

Examples:

0.1
0.7
0

Nucleus sampling parameter (0 to 1)

Type: number

Examples:

0.9
0.1
1

Agent execution mode

Type: string

Valid values: subagent, primary, all

Examples:

subagent
primary
all

Agent color — named token from OpenCode or 6-digit hex color (#RRGGBB)

Type: string | string

Examples:

primary
#ff6600

Maximum execution steps (positive integer)

Type: integer

Examples:

10
50

Hide agent from UI

Type: boolean

Examples:

true
false

Disable this agent overlay

Type: boolean

Examples:

true
false

Skills enabled for this agent

Type: string[]

Examples:

[
"ce:plan",
"ce:review"
]

Permission overrides per tool

Type: object

Examples:

{
"edit": "allow",
"bash": {
"curl": "allow",
"rm": "deny"
}
}

OpenCode-specific routing block. When present, model/variant here override the flat model/variant fields for OpenCode only; the flat fields remain the harness-neutral default and still apply to Pi. variant is bound to whichever layer supplies model: it is used only from that layer or a more specific one, and is dropped when a more specific layer sets (or nulls) model without repeating the variant. A variant with no model anywhere in the merged overlay is a config-load error raised by the routing resolver, not a parse-time error.

Type: object

Examples:

{
"model": "anthropic/claude-opus-5",
"variant": "high"
}
{
"model": null
}

Model identifier in provider/model format, or null to inherit parent model

Type: string | null

Examples:

anthropic/claude-sonnet-5
null

Model variant identifier

Type: string

Examples:

v2
extended

Pi-specific routing block. When present, model/thinking here override the flat model field and the legacy pi_subagents.<name>.thinking value for Pi only; the flat model field remains the harness-neutral default and still applies to OpenCode. Unlike OpenCode’s variant, thinking is independent of model: it applies to whatever model the delegate ends up running, including one inherited from the parent session, so thinking with no model anywhere in the merged overlay is valid and never a config-load error.

Type: object

Examples:

{
"model": "anthropic/claude-opus-5",
"thinking": "high"
}
{
"model": null
}

Model identifier in provider/model format, or null to inherit parent model

Type: string | null

Examples:

anthropic/claude-sonnet-5
null

pi-subagents reasoning effort level for exported persona frontmatter

Type: string

Valid values: off, minimal, low, medium, high, xhigh, max

Examples:

off
medium
high

JSON Schema URL for IDE autocomplete. The value is informational only — the loader does not fetch or validate against it. Add this to enable IDE schema activation and field-level autocomplete in editors that support JSON Schema (VSCode, Zed, IntelliJ).

Type: string

Examples:

"$schema": "https://fro.bot/systematic/schemas/v3/systematic-config.schema.json"

Per-agent configuration overlays keyed by bundled agent name (bare or qualified category/name). Unknown keys are rejected with a Zod parse error, except that a project config entry under an unknown key is discarded before that check if stripping trust-protected fields leaves it empty. To overlay a user-defined agent, configure it through OpenCode-native config (.opencode/opencode.json) instead. Trust-protected fields (model, variant, skills, permission, opencode, pi) are only valid in user config or OPENCODE_CONFIG_DIR config — a project config setting them has that field ignored with a warning; other fields in the same overlay still apply.

Type: object Default: {} Examples:

{
"correctness-reviewer": {
"temperature": 0.1
}
}
{
"review/correctness-reviewer": {
"temperature": 0.1
}
}
{}

Per-entry overlay fields are documented in the Agent/Category Overlay Fields section above.

If stripping trust-protected fields from a project-config entry leaves it empty, the entry is dropped instead of merged, so an earlier same-key entry from user (or active-profile) config keeps its nonprotected fields. An entry the project explicitly sets to {} is not emptied by stripping, so it merges as normal and replaces that earlier entry’s nonprotected fields, carrying over only the protected ones — custom config, if it also sets the key, is merged afterward and still applies on top regardless.

Per-category configuration overlays keyed by category name. Trust-protected fields (model, variant, skills, permission, opencode, pi) are only valid in user config or OPENCODE_CONFIG_DIR config — a project config setting them has that field ignored with a warning; other fields in the same overlay still apply.

Type: object Default: {} Examples:

{
"review": {
"model": "anthropic/claude-opus-5"
}
}
{}

Per-entry overlay fields are documented in the Agent/Category Overlay Fields section above.

If stripping trust-protected fields from a project-config entry leaves it empty, the entry is dropped instead of merged, so an earlier same-key entry from user (or active-profile) config keeps its nonprotected fields. An entry the project explicitly sets to {} is not emptied by stripping, so it merges as normal and replaces that earlier entry’s nonprotected fields, carrying over only the protected ones — custom config, if it also sets the key, is merged afterward and still applies on top regardless.

Named routing-only overlay bundles, selectable by name via the profile field. Valid in user config and OPENCODE_CONFIG_DIR config. A project config may always select a profile, but may only define this field when the user sets allow_project_profiles; a project-defined bundle is advisory and fills only routing the user has not set.

Type: object Default: {} Examples:

{
"personal": {
"agents": {
"correctness-reviewer": {
"model": "openai/gpt-6-astra"
}
}
}
}
{}

Each named bundle’s agents/categories entries have the same shape as the top-level agents/categories overlays above, restricted to a routing-only field subset — see Profile Bundle Overlay Fields below.

Selects a named entry from this source’s profiles map as the routing overlay bundle to apply. null explicitly selects the base config (no profile), which still wins over a lower-priority source’s selection. Omitting the field entirely defers the decision to a lower-priority source.

Type: string | null Examples:

personal
null

Array of bundled skill names to disable globally. Unknown skill names are rejected at parse time.

Type: string[] Default: [] Examples:

[
"ce:plan",
"ce:review"
]

Array of bundled agent names (bare or qualified category/name) to disable globally. Unknown agent names are rejected at parse time.

Type: string[] Default: [] Examples:

[
"previous-comments-reviewer",
"cli-readiness-reviewer"
]
[
"review/security-reviewer"
]

Array of command names to disable globally

Type: string[] Default: [] Examples:

[
"deprecated-migration-helper"
]

Bootstrap prompt configuration

Type: object Default: {"enabled":true} Examples:

{
"enabled": true
}
{
"enabled": false,
"file": ".opencode/custom-prompt.md"
}

Enable bootstrap prompt injection into every conversation

Type: boolean Default: true

Examples:

true
false

Path to a custom bootstrap prompt file

Type: string

Examples:

~/.config/opencode/bootstrap.md

Workflow guard configuration

Type: object Default: {"mode":"observe","debug":false} Examples:

{
"mode": "observe",
"debug": false
}

Pi-native pi-subagents export field overlays (thinking, max_turns, tools, skills). Category values apply first; per-agent values override. No model field — model stays in the categories/agents overlay.

Type: object Default: {"categories":{},"agents":{}} Examples:

{
"categories": {
"research": {
"thinking": "high"
}
},
"agents": {
"repo-research-analyst": {
"max_turns": 10
}
}
}

Per-category pi-subagents export overlays keyed by category name

Type: object Default: {}

Examples:

{
"research": {
"thinking": "high"
}
}
{}

Per-agent pi-subagents export overlays keyed by bundled agent name

Type: object Default: {}

Examples:

{
"repo-research-analyst": {
"max_turns": 10
}
}
{}

Per-entry overlay fields are documented in the Pi-subagents Overlay Fields section below.

Register skills discovered from user/project skill directories (OpenCode config and other agent-harness-standard locations) as slash commands. Default true.

Type: boolean Default: true Examples:

true
false

Allow a repository’s project config to define its own profiles map. User-owned only — only valid in user config or OPENCODE_CONFIG_DIR config; a project config setting this field has it ignored (a project cannot grant itself a permission it does not already have). Defaults to false.

Type: boolean Default: false Examples:

true
false

Each named bundle under profiles may set agents.<name> and categories.<name> entries using a routing-only subset of the Agent/Category Overlay Fields: model, variant, temperature, top_p, and the harness routing blocks opencode/pi. Non-routing fields (permission, skills, mode, hidden, disable, steps, color) are rejected.

Model identifier in provider/model format, or null to inherit parent model

Type: string | null

Examples:

anthropic/claude-sonnet-5
null

Model variant identifier

Type: string

Examples:

v2
extended

Sampling temperature (≥0; 0 = deterministic)

Type: number

Examples:

0.1
0.7
0

Nucleus sampling parameter (0 to 1)

Type: number

Examples:

0.9
0.1
1

OpenCode-specific routing block. When present, model/variant here override the flat model/variant fields for OpenCode only; the flat fields remain the harness-neutral default and still apply to Pi. variant is bound to whichever layer supplies model: it is used only from that layer or a more specific one, and is dropped when a more specific layer sets (or nulls) model without repeating the variant. A variant with no model anywhere in the merged overlay is a config-load error raised by the routing resolver, not a parse-time error.

Type: object

Examples:

{
"model": "anthropic/claude-opus-5",
"variant": "high"
}
{
"model": null
}

Model identifier in provider/model format, or null to inherit parent model

Type: string | null

Examples:

anthropic/claude-sonnet-5
null

Model variant identifier

Type: string

Examples:

v2
extended

Pi-specific routing block. When present, model/thinking here override the flat model field and the legacy pi_subagents.<name>.thinking value for Pi only; the flat model field remains the harness-neutral default and still applies to OpenCode. Unlike OpenCode’s variant, thinking is independent of model: it applies to whatever model the delegate ends up running, including one inherited from the parent session, so thinking with no model anywhere in the merged overlay is valid and never a config-load error.

Type: object

Examples:

{
"model": "anthropic/claude-opus-5",
"thinking": "high"
}
{
"model": null
}

Model identifier in provider/model format, or null to inherit parent model

Type: string | null

Examples:

anthropic/claude-sonnet-5
null

pi-subagents reasoning effort level for exported persona frontmatter

Type: string

Valid values: off, minimal, low, medium, high, xhigh, max

Examples:

off
medium
high

The pi_subagents.categories and pi_subagents.agents sections share the same set of per-entry overlay fields. Each key under pi_subagents.categories or pi_subagents.agents is an object with the following fields.

pi-subagents reasoning effort level for exported persona frontmatter

Type: string

Valid values: off, minimal, low, medium, high, xhigh, max

Examples:

off
medium
high

pi-subagents maximum turns for a delegated persona (0 = unlimited)

Type: integer

Examples:

0
10
25

pi-subagents comma-selector tool string (built-ins, */all/none, or extension selectors)

Type: string

Examples:

*
read,grep,glob
none

pi-subagents skills selector: true (all) or a comma-separated list of skill names

Type: boolean | string

Examples:

true
ce:plan,ce:review

The schema’s $id points to the canonical online URL above, so IDEs that prefer fetching the canonical schema may attempt to reach the docs site even when the bundled npm copy is also on disk. If you need strict offline behavior, configure your editor to associate systematic.{json,jsonc} with node_modules/@fro.bot/systematic/dist/schemas/systematic-config.schema.json directly.