Files
chemenu/docs/explanation/configuration-tiers.md
T
2026-10-11 21:15:40 +02:00

5.7 KiB

Configuration tiers

Settings are split into three tiers: what belongs to the instance (wikitool.json, committed), what belongs to one checkout (.wikitool-local.json, gitignored), and secrets, which sit outside the working tree and are reached only by reference. The files, their fields and the command that writes them are in the configuration reference; the rules an agent follows are in AGENTS.md § Settings. This page is about why it is built this way, and what it does not do.

Three kinds of data that used to share files

Before the tiers, every feature kept its settings in a gitignored file of its own. Three kinds of data ended up mixed in them:

  • What belongs to the instance. The weekly review's thresholds are a decision about the corpus, and every clone of it should make the same one. Gitignoring them meant each clone had to be told again.
  • What belongs to one checkout. A tool path, the push URLs a checkout may publish to, the consent to record telemetry on this disk. Committing any of these would hand every other clone an answer that is wrong for it rather than missing.
  • Secrets. A tracker token sat in plain text in the working tree, protected only by .gitignore - one wrong pattern away from a commit, and in plain view of every agent that read the file in the course of ordinary work.

The tiers give each kind one place, declared per field, so the question "where does this go" is answered once, by whoever declares the field, rather than each time by whoever writes a value.

One place per value, not an override

A familiar design lets the local file override the committed one. It is deliberately not this one: a value in the wrong file is an error. With two possible sources for one value, every reader first has to work out which one wins, and the answer differs between clones - a reviewer looking at wikitool.json would no longer know what the instance actually runs with. One place per value keeps the committed file a true statement about every clone.

What runs, and where it pushes, never travels with a pull

A field that names a program to run, or that arms or loosens a gate, is never in the committed file - not even as a reference, since a resolved reference is still a value someone else chose. Otherwise a git pull from another clone could change what wikitool executes on this machine, or where publish sends this checkout's content. The rule is checked when a field is declared, in code, so no reader has to remember it; the reasoning is the same as for the gates (why-gates-are-code.md). The same fields are also not written by config set: like the Publish-Remote Gate's allowlist, they change by the user's own edit.

References instead of values

A secret is only ever a reference, and a private value - a host name, an internal URL - may be one. A reference names where the value lives: a name, which resolves through a WIKITOOL_SECRET_<NAME> variable or a secrets file in the user's own configuration directory, or an op:// URI, which resolves through the 1Password CLI.

That is what makes the committed file portable. An instance that is public commits wikitool.json publicly, so a private value can never sit there as text; as an op:// reference it can, and every machine signed in to the same 1Password account resolves it to the same value. In CI, op runs with a service-account token and resolves the same op:// references. Alternatively, 1Password's own action resolves values into variables before wikitool starts, which serves references by name. This repository's CI does the second.

A reference is resolved only when a command asks for that one value, never when the files are read, and the value stays in that process's memory. doctor reports whether each reference resolves, never what it resolves to.

One secret has no reference at all: the session raw login stores for a website behind a login. The user never types it, so nobody could put it into a secrets source by hand. raw login writes it into the secrets file itself, under a name derived from the host, and raw fetch finds it by that name. It never goes to 1Password, because it changes with every sign-in and the stack only ever reads from there. So it sits in the one source the 1Password safeguards below do not cover.

What this does not protect against

Keeping secrets out of the tree protects against three things: a secret being committed, a secret sitting in a file an agent reads while doing ordinary work, and a secret appearing in wikitool's output.

It does not protect against an agent with shell access that sets out to read one. If 1Password is unlocked in the terminal, the agent can run op read itself. It can read the secrets file. In general, whatever the wikitool process can read in a user account, an agent running in that same account can read too. The stack does not claim otherwise.

What limits the exposure without preventing it:

  • a 1Password vault of its own for this instance's entries, so an unlocked op reaches nothing else;
  • the approval prompt of the 1Password desktop app, which asks a human before the CLI may read anything - per terminal session, not per value, so it stops a session nobody approved and not one that already was;
  • in CI, a service account that sees only that vault.

None of these covers a website session from raw login, which lives in the secrets file only. Whoever can read that file can use the website as the user, until the session expires or the user signs out there.

The rule in AGENTS.md § Settings - a secret never passes through the agent - is an instruction, not a barrier. The tiers make following it the easy path. They do not make breaking it impossible.