Configuration reference
Reference. Everything Procoder reads from a repository lives under
.procoder/ — plain files, made to be edited, and always winning over
built-in defaults (the D-OVERRIDE rule).
.procoder/config.toml
[git]
# Working directly on the default branch: "report" (default) or "block".
default_branch_policy = "report"
# Oversized-file threshold for the gate, in MB. Default 5.
max_file_mb = 5
# The commit interception: "block" (default) stops a commit whose gate
# has blocking findings, "report" prints them and lets it through,
# "off" skips the check.
commit_gate = "block"
[gate]
# How much of Procoder this repository is subject to: "adopted" or
# "universal". Normally omitted — Procoder decides from the repository
# itself, and this only forces the answer.
#
# "adopted" everything runs.
# "universal" only the checks that are true in any repository: secrets,
# oversized files, conflict markers, junk files, and AI
# attribution in the commit message. Procoder's own
# conventions — formatting, linting, the agent layer, the
# planning chain, templates, docs, debt, the suite — do not
# run, and the content checks see only the lines the commit
# wrote.
#
# Setting this to "universal" in a repository that HAS adopted Procoder
# really does reduce the gate; it is not just a label.
scope = "adopted"
[lint]
# Lint findings in the gate: "report" (default) or "block".
policy = "report"
[test]
# The suite verdict: "report" (default) or "block". Under "block",
# `todo close`, `backlog close story` and the commit gate refuse while
# the suite is red. At the gate the run is narrowed to the packages the
# commit touches and to the ecosystems it is written in — the whole
# suite cold is a minute on a repository this size, one package a second.
#
# A suite that could NOT run blocks under either setting: this governs
# whether a failing test stops a commit, and "no answer" is not a
# verdict it has an opinion about.
policy = "report"
[maintain]
# Complexity findings in the gate: "report" (default) or "block".
# Off by default — they are judgement calls, and a threshold that blocks
# by surprise stops people committing to the very file that needs the
# refactor.
policy = "report"
[docs]
# The documentation obligation in the gate: "report" (default) or
# "block". Off by default — procoder never blocks a repo by surprise.
policy = "report"
[sprint]
# The retro gate: unset, `sprint open` refuses while the last closed
# sprint's retro is empty. Set to "off" to opt out.
retro = "off"
[release]
# The handles the changelog's credit rule does not ask about: whose
# release notes these are. Thanking yourself in your own notes is noise.
#
# Configured rather than discovered. `gh api user` answers only where a
# person is logged in; in CI the token is an app installation token with
# no user behind it and returns 403, which made the check unrunnable in
# the one place it most needed to run. "Whoever triggered the workflow"
# is worse — on a contributor's pull request that is the contributor,
# who would then be excluded from the credit they are owed.
maintainers = ["your-handle"]
# The version-bearing files `procoder release` verifies stay in sync.
# Unset, the version-sync leg reports that it verified nothing.
files = ["README.md", "docs/index.md"]
[bench]
# Benchmark regressions beyond this percentage are marked and exit 1.
# Default 10.
threshold = 10
[ci]
# Actions pinned to mutable refs: "report" (default) or "block".
pin_actions_policy = "report"
[maintain]
# Complexity/length thresholds for `procoder maintain`. Defaults shown.
gocyclo = 15
funlen_lines = 80
funlen_statements = 50
[debt]
# Comment marker `procoder debt` harvests. Default shown.
marker = "debt:"
[ask]
# Pending questions: "report" (default) lists them and leaves the gate's
# verdict alone; "block" refuses the commit until a human has answered.
policy = "report"
# A question's identity is its words, not its line breaks. Recording an
# answer under a section that a formatter later rewrapped keeps the answer
# valid — the question was not reworded. Rewording it asks it again, and an
# answer recorded by an older build (whose key hashed the text as written)
# still reads as an answer.
[version]
# "warn" (default) reports a newer release at session start; "off" silences
# it for CI and scripted runs. There is deliberately no third value: a
# setting that upgraded without asking would remove the consent the
# upgrade is built on.
check = "warn"
[service]
# Overrides the repository identity procoder computes. Unset by default.
repo = "acme/widgets"
A setting procoder does not know
An unrecognised key blocks. A key that does nothing while its writer believes it is in force is the failure this whole feature would otherwise introduce, so silence is not an option.
The finding names both reasons a key can be unrecognised, because only one
of them is yours to fix. A typo is: correct the spelling. A key added
in a later release is not — you spelled it correctly, this build is
simply older, and no edit to the file will help. The finding says which
build is doing the not-knowing and points at procoder self-upgrade.
That distinction is not cosmetic. An instruction nobody can follow is how
--no-verify becomes muscle memory, which is the failure behind both #172
and #185 — and it happened here, with a key added in one commit reported
unknown by the plugin binary from the release before it.
.procoder/context.md
The project's shared vocabulary: a ## <term> heading per entry with a
one-paragraph definition beneath. Written by hand or by an agent, never
generated — the value is what the team agreed to call something.
procoder context list and check read it, and procoder spec check notes
when a spec seems to be describing a term already defined. None of those
blocks anything — vocabulary is not grounds for failing a build.
procoder context add does refuse an entry with no definition, or a term
already defined under another spelling. Neither is a wording judgement, and
both exit 2.
A glossary that exists and cannot be read is reported as such, never as an absent one.
Adopted and universal repositories
Procoder runs two gates, and which one you get is decided from the repository in front of it — never from the machine, because a contributor's laptop looks identical in their own repository and in somebody else's.
A repository has adopted Procoder if it has a .procoder/ directory,
or an AGENTS.md that names Procoder. Everything runs, exactly as it
always has.
A repository with neither is somebody else's, and gets the universal
gate: a credential, an oversized blob, a conflict marker, a junk file, and
an AI-attribution trailer nobody wrote. Those are wrong in any repository
whatever its house style. Procoder's own conventions do not run, because
that project never asked for them — its formatter may be Biome, its
AGENTS.md is its own, and its test command is not Procoder's business.
In the universal gate the checks that read file content see only the lines the commit added or changed. A secret four thousand lines from your diff is not yours to answer for. Checks about a file's existence — oversized, junk — do not narrow, because a file the commit introduces is the commit's, all of it.
Every run says which mode it was in. A quieter gate that does not announce itself is indistinguishable from a clean one:
gate scope: universal (no .procoder/ and no AGENTS.md naming procoder)
procoder's own conventions are NOT checked here — this repository has not adopted it.
For the full gate: PROCODER_GATE_SCOPE=adopted, or adopt procoder in this repository.
PROCODER_GATE_SCOPE takes the same two values as [gate] scope, for a
fork that cannot carry configuration without that itself being a change the
contributor does not want to make. The config file wins over the
environment variable: the file is the repository's deliberate choice, the
variable is whichever shell this happens to be.
The reasoning is in ADR 0005.
.procoder/github/REVIEW.md and .procoder/github/LESSONS.md
The catch-first-and-learn pair. REVIEW.md is the pre-PR self-review
rubric a fresh-context reviewer reads against the branch diff before any
PR is opened. LESSONS.md is the ledger of findings that escaped anyway:
each entry names the layer that should have caught it and the adaptation
that now does (procoder lessons flags unlearned entries). Defaults from
procoder templates; both are the repo's to grow.
.procoder/PRINCIPLES.md
The engineering principles injected at session start (see
procoder principles). Absent, Procoder's default build philosophy
applies; present, the repo's file replaces it wholesale — the override is
total, not merged.
Rules files (prose + machine-read lists)
.procoder/docs/RULES.md
Documentation rules. Machine-read sections (one - item per line):
## Required docs— files that must exist (default: README.md, CHANGELOG.md)## Required badges— keywords that must appear inside a badge image on the README's first screen (default: ci, license)## README first screen— required first-screen elements (default: usp, badges, quick start)## Version-tracked docs— pages whose first screen must carry the current version; a release that skips one blocks the gate (default: README.md, docs/index.md)## README must mention— the feature families the README's narrative must carry (empty by default; when filled, a family the front page stops telling blocks the gate — the mechanical floor against README rot)
Everything under ## Guidance is prose the agent follows when it writes,
not something the binary parses. The shipped default carries the
Divio documentation system —
four kinds of document (tutorial, how-to guide, reference, explanation),
never mixed, the kind decided before the first line — plus the writing
rules that follow from it: answer first, examples over prose about
examples, real names rather than foo, short sentences, scannable
structure, and an explicit "common pitfalls" list wherever a feature has a
known misuse. Replace it with your own house style; the repo's copy wins.
In-file exemptions
Two checks can be waived from inside the file itself, and both demand a reason in the same line:
gitleaks:allow— this secret finding is a false positive (a pinned SHA, a fixture)..gitleaksignoredoes the same at repository scope.procoder:allow-conflict-markers <reason>— this file shows merge conflict markers on purpose. It exempts the whole file, so keep it to documents whose subject is conflicts; a real conflict in an exempt file goes unreported.
The reason is not decoration. A bare
<!-- procoder:allow-conflict-markers --> exempts nothing, because a
token with no reason is someone silencing a check rather than
documenting an exception.
Markers inside a fenced code block are not exempt on their own. A real conflict lands inside a fence often enough that skipping fences would be a silent miss.
.procoder/security/RULES.md
Security rules the agent follows: what blocks (secrets always; SAST ERROR; CVSS ≥ 7.0), how to review from the index's entry points, and what never happens (echoing a secret, silencing a scanner).
.procoder/github/WORKFLOW.md
The team workflow the pr/merge skills follow: worktree-first feature work (a git practice the skills describe — Procoder itself creates and removes nothing), the merge-watcher protocol (calibrate, poll per job, fail fast, report on change), and post-merge cleanup.
.procoder/docs/mermaid.json
The shared Mermaid theme applied when compiling diagrams.
Templates
.procoder/github/PULL_REQUEST_TEMPLATE.md— the master the pr skill fills; mirrored to.github/PULL_REQUEST_TEMPLATE.md(GitHub reads only that path), and drift between the two blocks the gate..procoder/github/COMMIT_TEMPLATE.md— registered withgit config commit.template.
procoder templates prints the default for anything missing; the agent
writes it — the binary creates no files itself.
Work state
Committed, reviewable Markdown — the record of what is being built, kept where the code is:
.procoder/specs/,.procoder/plans/,.procoder/todo/— the design documents, the implementation plans, and the standalone task list..procoder/backlog/—milestones/,epics/,stories/, andsprints/, the project layerprocoder backlogandprocoder sprintread and write through their controllers..procoder/adr/— the numbered architecture decision records; a changed mind supersedes a record, it never rewrites one.
Derived state
.procoder/index/— the code index (gitignored: derived, per-machine). The write hook keeps it current; the gate rebuilds it when stale..procoder/bench/baseline.txt— the benchmark baseline thatprocoder benchcompares against. Committed rather than derived: it is a deliberate decision, written only bybench --save, and a baseline recorded on another GOOS/GOARCH compares with a warning.
Three of these are written by procoder into the repository it governs and
belong in .gitignore. procoder git names any that are missing:
.procoder/index/
.procoder/state/
.lycheecache
The last is lychee's link cache, left behind by procoder docs
--external. The rest of .procoder/ is committed on purpose — it is the
repository's own rules, specs and backlog, and a teammate who clones
without them gets a different gate.
[security]
| key | values | default | meaning |
|---|---|---|---|
sast_blocks_at |
INFO, WARNING, ERROR |
ERROR |
the lowest semgrep severity that stops a commit |
ERROR is the level semgrep reserves for findings it is confident about.
Lowering the bar to WARNING makes more findings block — a strengthening,
and silent. Raising it makes fewer block, which is a relaxation and prints
on every gate run.
[infra]
| key | values | default | meaning |
|---|---|---|---|
terraform_binary |
auto, terraform, tofu |
auto |
the tool that formats and validates Terraform directories |
auto reads each directory: a lockfile or installed providers from
OpenTofu's registry select tofu, from Terraform's select terraform, and
with neither an installed tofu is preferred. Pin a tool when that guess
is wrong for you — a directory with no lockfile yet, or an estate that uses
one tool everywhere. The two tools resolve providers from different
registries, so validating with the wrong one fails on providers, not on
the code.
Seeing the effective configuration
procoder config prints every setting, its value, and where that value
came from — default, or the config file and line. A setting weaker than
its default is marked, so a reader of an unfamiliar repository can tell at
a glance which of Procoder's defaults are still in force.
A setting Procoder cannot apply — a mistyped key, a value of the wrong kind — is not silently ignored. It is reported with its line number and it blocks, because a config that quietly reverts to defaults lets a team believe a setting is in force when it never was.
[tools]
Choose among the tools Procoder ships, by language:
[tools]
js = "biome" # instead of the default, prettier
One key per language, covering every extension that language owns — js
covers .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts and .cts.
A repository names a tool; it does not name a binary and an argv. Procoder owns the invocation, and that is what keeps the print-don't-write contract a guarantee: a formatter is only on the menu if it can emit the formatted source on stdout and leave the file alone. Tools that can only write in place — Laravel Pint, phpcbf, php-cs-fixer — are absent for that reason and not for any other.
Naming a tool Procoder does not ship is reported with the list of what it does ship, and blocks. The file is still formatted by the default: a mistyped tool name is a reason to tell somebody, never a reason to stop reading their code.
.procoder/templates/
The nine templates that drive the quality chain — spec, plan, ADR, todo,
milestone, epic, story, sprint, bug — plus a changelog template, are the
repository's to replace. Put a file at .procoder/templates/<name>.md and
it wins; leave it out and Procoder's own is used.
An empty template file is an error, not a fallback. It blocks, and
Procoder uses its own template for that run while saying so. The reason is
that an empty file is indistinguishable from an emptied one — written out
by an editor, truncated by a bad merge, or replaced by a pipeline that
printed a header and nothing else. (procoder format did exactly that on
its already-formatted path until the contract changed: stdout now carries
the bytes that belong in the file for every verdict, and the verdict line
went to stderr where it cannot overwrite anything.) Either way, falling
back quietly would mean a team discovers their customised template is gone
when their next story comes out in Procoder's shape instead of theirs.
.procoder/lint/RULES.md
The lint domain reads rules the same way docs and security do: prose
for the agent, with list sections a machine reads. A section that is
present replaces the default; a section that is absent keeps it.
## checks
- `readability-*`
- `bugprone-*`
That list replaces Procoder's curated clang-tidy families. Replace means
replace — a family left out does not survive. A project .clang-tidy
still wins over both: that is the tool's own configuration.
[planning]
| Key | Values | Default | Effect |
|---|---|---|---|
method |
procoder, bmad |
procoder |
Who owns the planning artifacts. |
procoder is the chain under .procoder/ — specs, plans, backlog,
sprints.
bmad means a separately installed BMad
Method owns them, and
Procoder reads its artifacts instead: sprint-status.yaml for sprint
state, and the installation's own output_folder setting for where to
look. procoder status reports that sprint; procoder doctor names the
installed version. Procoder never writes into those directories — BMad
owns what it wrote.
The setting moves planning and nothing else. The gate, the suite, formatting, the release controller, the debt ledger, security and docs run identically either way and reach the same verdict about the same code. A test asserts that every finding the gate makes about the code is identical across both settings, so the seam cannot drift.
Setting bmad with no BMad installed is a blocking finding naming both,
rather than a silent fall back to Procoder's own chain: a repository that
chose one methodology must not be governed by the other without being
told.
[learn]
| Key | Default | What it does |
|---|---|---|
record |
false |
Append one timing record per command run to .procoder/state/learn.jsonl. Off until asked: no repository starts measuring because it upgraded. |
min_samples |
20 |
How many recorded runs procoder learn propose wants before it proposes anything. |
The records are gitignored state, not repository content, and hold a command name, a duration and an exit code — nothing about a file's contents and nothing identifying a person.
[service]
| Key | Default | What it does |
|---|---|---|
repo |
computed | The repository's stable name, overriding the one procoder works out. |
mode |
off |
local runs commands through the daemon when one is listening. |
exec |
false |
Opens the second socket, which serves the commands that run things. |
mode
A machine is one or the other. off is the default and stays the default
— no repository changes behaviour because it upgraded — and every command
runs in this process, with no daemon and no setup, in CI and on a fresh
clone.
local means the daemon is the path. There is no fallback. A daemon
that is not running, is from another build, or goes away mid-request is an
error: the command does not run, and you are told what happened and how to
fix it.
That is a deliberate choice and not an oversight. A silent fallback would mean two possible answers to "where did this verdict come from", indistinguishable from the outside — and a machine configured for the daemon could spend weeks never reaching it with nothing saying so. An error is louder and shorter.
procoder init asks which this machine should be rather than choosing for
you, and a value that is neither off nor local leaves the machine
where it was rather than in a state nobody chose.
procoder serve is the daemon itself — see
Commands.
exec
Four commands run what a repository — or a prior agent session — declared:
run --exec, evidence record, init --yes and self-upgrade. They are
never served on the ordinary socket, whatever this key says.
exec = true opens a second socket for them alone, at
~/.procoder/run/procoder-exec.sock, whose address the hooks are never
told. That separation is the point rather than a detail: the socket's 0600
mode authenticates the user, not the process, so every process running
as you can open it — including an agent session's own shell. A path that
runs an agent-written command must not be reachable by something running
unattended.
Leave it false unless you specifically want a caller to be able to run
those four. On a machine set to mode = "local" they are refused while
this is false — not quietly run in this process, because there is no
fallback anywhere in this design. Set mode = "off" to run them as the
CLI.
Procoder needs a name for this repository that means the same thing on somebody else's machine. A filesystem path does not: the same repository lives at a different path for every person who clones it.
So it works one out, down a ladder, and procoder config prints both the
answer and the rung that produced it:
repo identity host/owner/repo (origin remote)
| Rung | When it answers |
|---|---|
[service] repo |
You set it. Nothing below is consulted. |
| origin | There is an origin remote. |
| first remote | There is no origin; the alphabetically first remote wins, and the line names it. |
| root path | There are no remotes at all. The resolved absolute path. |
Remote URLs are normalised to host/owner/repo, so
git@host:o/r.git, https://host/o/r.git and ssh://git@host/o/r are
one identity rather than three.
origin beats an alphabetically earlier remote deliberately. Pure
alphabetical order is simpler and wrong: a colleague who adds a personal
remote named fork would key the same repository differently from
everybody else, which defeats the one thing an identity is for.
Set repo when the computed answer is wrong for you — a monorepo serving
several products, a mirror whose remote is not the name anybody uses, or
a checkout with no remote at all that you would rather not identify by
path.