Every settings field declares its tier; a value in the wrong file is an error naming
file, key and tier. A field naming a program or affecting a gate cannot be declared
instance. Secrets are references only: {"ref": "<name>"} via WIKITOOL_SECRET_<NAME> or
a user-only secrets file, {"ref": "op://..."} via op read (locked 1Password: exit 42).
The preflight records op as an optional tool. doctor gains one line per tier, docs
verify checks a committed wikitool.json for plaintext secrets, CI resolves a real
op:// value through 1password/load-secrets-action. No feature reads its settings
this way yet; without the two files every other command is unchanged.
Files changed:
- .gitea/workflows/ci.yml
- .gitignore
- AGENTS.md
- CHANGES.md
- INSTALL.md
- VERSION
- docs/explanation/README.md
- docs/explanation/configuration-tiers.md
- docs/how-to/README.md
- docs/how-to/use-1password-for-settings.md
- docs/reference/commands/README.md
- docs/reference/commands/configuration.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/types-instructions-and-docs.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/dev/doc-pull-through.md
- instructions/dev/testing-conventions.md
- instructions/preflight.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/config_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/config.py
- tools/chemenu/config_shapes.py
- tools/chemenu/prerequisites.py
- tools/chemenu/settings.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_settings.py
- tools/chemenu/toolpaths.py
- tools/preflight.ps1
- tools/preflight.sh
- tools/prerequisites.txt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
21 KiB
Configuration
The environment variables wikitool reads, the per-checkout files an operator may create at the
root of an instance, and the two tiered settings files. None of them is needed for an instance to work: an instance installed from a
release runs against the origin's public release feed with no configuration at all.
The shape of each file - its fields, what an absent or a malformed file means, an example - is
generated from the record wikitool itself reads the file with, so it cannot describe a field the
tool does not take. What a setting does around it is written by hand.
Three more files at the root are written by wikitool and never by hand: .wikitool-release.json,
.wikitool-kb.json and .wikitool-tools.json (AGENTS.md invariant 1).
The last one has its own section below, because the preflight that writes it is
something you run.
Contents
- Environment variables
- Telemetry
- Tool paths
- Allowed push URLs
- Task tracker
- MCP submissions
- Tiered settings
Environment variables
| Variable | Purpose | Fallback |
|---|---|---|
WIKI_AUTHOR |
Overrides the author name of new source pages | git config user.name. Without either, new stops with an ERROR |
WIKITOOL_SESSION_ID |
Scopes the Iteration Budget Gate to a task rather than a terminal, and names the directory a trace lands in | A session variable the harness sets itself, where one is known (for example CLAUDE_CODE_SESSION_ID), else the parent process id (instructions/session-setup.md) |
WIKITOOL_UPDATE_URL |
The release feed version check, version notes and dist upgrade --latest ask |
The value in .wikitool-release.json, else the origin's feed |
WIKITOOL_UPDATE_TOKEN |
A Gitea token for the release feed, sent only to downloads on the feed's own host | None. Not needed against the origin, only against a feed in a private repository |
WIKITOOL_TASKS_CONFIG |
Path to a tracker configuration that task, review and doctor read instead of .wikitool-tasks.json, for running one checkout against several trackers in turn |
.wikitool-tasks.json at the root. A path to a file that does not exist is an error, never "no tracker configured" |
WIKITOOL_SECRET_<NAME> |
The value behind a settings reference {"ref": "<name>"}, for CI and containers - <NAME> is the name upper-cased |
The secrets file in the user's configuration directory (below) |
CHEMENU_ROOT |
Which corpus the package works on, for a caller outside the checkout itself | The checkout the package sits in. tools/wikitool behaves the same without it |
WIKI_TRACE |
0/1 turns telemetry off or on, beating both the default and .wikitool-telemetry.json |
The installation-form default (below) |
WIKI_TRACE_DIR |
Writes traces somewhere other than reports/telemetry/ |
reports/telemetry/ |
WIKI_TRACE_CONTENT |
0 keeps only lengths and digests of prompts and replies instead of their text |
Text is kept |
WIKI_TRACE_MAX_CONTENT |
Per-attribute content cap in characters | 60 KiB |
WIKI_TRACE_MAX_SESSION_BYTES |
Byte cap per session trace; beats the file's max_session_bytes |
5 MiB |
WIKI_TRACE_KEEP_SESSIONS |
How many session directories retention keeps; beats the file's keep_sessions |
250 |
Telemetry
Telemetry is off in an instance. Every instance carries the .wikitool-release.json of its
release, and that is how chemenu.telemetry.policy recognises that nobody asked for telemetry. The
same holds for every further clone of the instance, because the file is committed with the first
publish. Only a clone of the origin repository, used to develop the stack, has no stamp. There the
traces are the instrument the stack measures itself with, and they are on.
To turn the default around, create .wikitool-telemetry.json at the root. It is per checkout and
gitignored, with no .template, like .wikitool-remotes.json.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema |
any | no | - | Accepted and not read. Conventionally 1; no version of this file is told apart by it yet. |
enabled |
boolean | no | the installation-form default | Telemetry on or off. WIKI_TRACE beats it in both directions. Any other value is read by its truthiness, so "no" means on. |
max_session_bytes |
positive integer | no | 5242880 |
Byte cap per session trace. WIKI_TRACE_MAX_SESSION_BYTES beats it. A number written as a string is accepted. |
keep_sessions |
positive integer | no | 250 |
How many session directories retention keeps. WIKI_TRACE_KEEP_SESSIONS beats it. A number written as a string is accepted. |
- Absent: The installation-form default applies: on in a clone of the origin repository, off in an instance.
- Malformed: Never an error. Unreadable JSON or a non-object counts as an absent file, and a cap that is not a positive integer as an absent cap.
doctorreports each such deviation as aWARNundertelemetry, naming the field. - Unknown keys: Ignored;
doctorreports them as aWARN.
Example:
{
"enabled": true,
"max_session_bytes": 5242880,
"keep_sessions": 250
}
wikitool doctor reports the current state: on or off, why, and how much has been recorded against
both caps. What the caps do and what never reaches a trace:
telemetry-and-evaluation.md.
Tool paths
The preflight writes this file, not you. .wikitool-tools.json at the root holds the absolute
paths of Python, git and ripgrep, as the preflight found them on this machine - and of the
1Password CLI op when it is installed. op is optional: the preflight records it when it finds
it, reports ABSENT when it does not, and never asks for it. Only an op:// reference in the
tiered settings needs it. wikitool starts git
and rg from there instead of from PATH. It is per checkout and gitignored, because a path on one
machine says nothing about the next.
If the file is missing or incomplete, tools/wikitool does not start (exit 42) and names the
preflight. If a tool sits somewhere the preflight does not look, name it:
tools/preflight.sh --set rg=<path>, or in PowerShell
pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1 --set rg=<path>. Never edit the
file by hand. doctor reports under tool-paths whether every path still works.
Allowed push URLs
.wikitool-remotes.json at the root restricts where publish may push, per checkout and
gitignored.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema |
any | no | - | Accepted and not read. Conventionally 1; no version of this file is told apart by it yet. |
allowed_push_urls |
array of strings | yes | - | The push URLs publish may write to, compared with the URL the remote resolves to for a push - not with its name. An empty list allows no push at all. |
- Absent: Any remote is allowed; the Publish-Remote Gate does not apply.
- Malformed:
publishstops with an error before committing anything. A broken file is never read as "no restriction". - Unknown keys: Ignored.
Example:
{
"schema": 1,
"allowed_push_urls": [
"<url>"
]
}
With the file, publish refuses with exit 42 (the Publish-Remote Gate) before pushing to a URL not
on the list. Only you edit this file. The gate has no token and no flag, and an agent that edits the
list to get past a refusal is opening a gate on its own initiative.
Task tracker
Optional. The weekly review (tools/wikitool review, skill gtd-weekly-review) checks the
project pages under kb/gtd/ against a task tracker. Which tracker that is goes in
.wikitool-tasks.json at the root. Like the telemetry and remotes files, it is per checkout and has
no .template, and it is gitignored as soon as a token is in it.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema |
any | no | - | Accepted and not read. Conventionally 1; no version of this file is told apart by it yet. |
provider |
string | yes | - | Which adapter the review goes through. The provider's own settings sit in a section of the same name. |
thresholds |
object | yes | - | The weekly review's three thresholds. |
thresholds.stalled_waiting_days |
positive integer | yes | - | A waiting-for item older than this many days counts as overdue. A number written as a string is accepted. |
thresholds.unpaged_project_weeks |
positive integer | yes | - | A tracker project with no kb/ page is reported once it is older than this many weeks. A number written as a string is accepted. |
thresholds.someday_stale_months |
positive integer | yes | - | A someday/maybe item untouched for this many months counts as stale. A number written as a string is accepted. |
- Absent: No tracker is configured - a valid state. The weekly review has nothing to check against.
- Malformed: An error, and
doctorreportsFAIL. A broken file is never read as "no tracker configured". - Unknown keys: Ignored, so the section of a provider not chosen right now can stay in the file.
The superproductivity section. Connection settings for the Super Productivity adapter.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
access |
string | yes | - | Which way in: api, the local REST API (reads the current state and carries the write path), or snapshot, the newest backup snapshot (read-only). |
With access: "api":
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
api_base_url |
string | no | "http://127.0.0.1:3876" |
Where the app's REST API listens. An empty value means the default. |
api_token |
string | yes, not empty | - | The token from the app's settings. |
With access: "snapshot":
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
backups_dir |
string | yes, not empty | - | The directory the app writes its backup snapshots to; ~ is expanded. |
- Unknown keys: Rejected: a section names only one access path's own fields, so a
backups_dirbesideaccess: "api"is an error, not ignored.
{
"superproductivity": {
"access": "api",
"api_base_url": "http://127.0.0.1:3876",
"api_token": "..."
}
}
{
"superproductivity": {
"access": "snapshot",
"backups_dir": "~/.config/superProductivity/backups"
}
}
The caldav section. Connection settings for the CalDAV adapter.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url |
string | yes, not empty | - | The account's CalDAV calendar home set. A trailing / is added when missing. |
username |
string | yes, not empty | - | The login name, which can differ from the user id in the URL. |
app_password |
string | yes, not empty | - | An app password is recommended over the account password. |
inbox_list |
string | yes, not empty | - | The name of the one inbox list. |
someday_list |
string | yes, not empty | - | The name of the one someday/maybe list. |
exclude_lists |
array of strings | no | [] |
Existing plain task lists that are not projects. |
- Unknown keys: Rejected.
{
"caldav": {
"url": "https://<host>/remote.php/dav/calendars/<user>/",
"username": "<login>",
"app_password": "...",
"inbox_list": "Inbox",
"someday_list": "Someday",
"exclude_lists": [
"..."
]
}
}
A complete example:
{
"schema": 1,
"provider": "superproductivity",
"thresholds": {
"stalled_waiting_days": 14,
"unpaged_project_weeks": 3,
"someday_stale_months": 5
},
"superproductivity": {
"access": "api",
"api_base_url": "http://127.0.0.1:3876",
"api_token": "..."
}
}
The thresholds block is configuration, not schema. A value the provider cannot supply at all - a
waiting item with no follow-up date, a tracker project with no creation date - is not skipped silently
but reported as its own finding (waiting_no_follow_up/project_age_unknown).
Super Productivity
access is mandatory and has no fallback. It chooses between two mutually exclusive ways in:
-
access: "snapshot"for an instance run headless: it reads only the newest backup snapshot underbackups_dir. That works without the app running, but only reads. The tracker cannot be written from there. -
access: "api"for a desktop instance: it talks only to the local REST API. That answers only while the app runs, but it gives the current state and carries the write path.api_tokenis required, because every endpoint exceptGET /healthdemandsAuthorization: Bearer <token>.
What can write:
tools/wikitool new projectcreates a tracker project of the same name only withaccess: "api", and even there not automatically. The command stops (exit 42) and asks a human to create it, then continues with--resume.tools/wikitool task newcreates a single item without akb/page.tools/wikitool task close --idmarks an item done. It never deletes one.
With access: "snapshot", all three refuse completely with exit 1, because the tracker is read-only
from there. tools/wikitool task list --project only reads, so it answers with either access.
CalDAV
caldav is the standards-based second adapter (RFC 4791/5545). It is verified against Nextcloud
Tasks, with iOS Reminders as the mobile client, and built for that split: tick items off on the
phone, maintain them at the desk. Unlike Super Productivity, there is only one way in. CalDAV is
always a network access, so no access field is needed.
urlmay be an alias - Nextcloud accepts a short name there - because every later address comes only from thehrefs the server returns, never from this URL joined with a name.inbox_listandsomeday_listname the two fixed lists, exactly one of each per instance.- A list with a
VEVENTcomponent never counts as a project, soexclude_listsis needed only for plain task lists.
A project is a list whose only supported component is VTODO. tools/wikitool new project creates
it automatically with MKCALENDAR - without asking, unlike Super Productivity, because CalDAV has a
real create command for lists. The uniqueness check runs against every list in the account,
including excluded lists, the inbox, someday and mixed calendars. If a new name collides with any of
them, nothing is created and the collision is named. Renaming the existing list in Nextcloud stays a
manual step.
task new and task close work without restriction on caldav, which has no read-only mode like
access: "snapshot". When ticking an item off, task close changes only STATUS, COMPLETED,
PERCENT-COMPLETE, LAST-MODIFIED and DTSTAMP on the existing .ics resource. Every other
property, including an unknown X- property or an alarm, is kept unchanged. A resource that changed
since it was read (an ETag conflict) is not written, and the command stops with exit 1. Nothing is
ever deleted. The lists of finished projects stay after archiving, because the adapter never deletes
a list. Remove them by hand in Nextcloud when you want to.
MCP submissions
.wikitool-upload.json in the served corpus turns on the MCP read server's optional submit tool.
What the authentication in front of the server has to guarantee before you create it is in
Set up the MCP read server.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema |
any | no | - | Accepted and not read. Conventionally 1; no version of this file is told apart by it yet. |
identity_header |
string | no | "X-Forwarded-User" |
The request header the authenticating middleware sets to the submitter's identity on every request. An empty value means the default. |
max_bytes |
positive integer | yes | - | Largest single submission, in bytes. A number written as a string is accepted. |
allowed_extensions |
array of strings | yes | - | The file extensions submit accepts, each starting with .. Compared case-insensitively. Entries are read as strings and lower-cased; duplicates collapse. |
quota |
object | yes | - | Per-submitter limits over any rolling 24 hours. |
quota.submissions_per_day |
positive integer | yes | - | Submissions one submitter may make. A number written as a string is accepted. |
quota.bytes_per_day |
positive integer | yes | - | Bytes one submitter may submit. A number written as a string is accepted. |
- Absent: The
submittool is not registered: the write path does not exist. - Malformed: An error. The MCP server does not start and
doctorreportsFAIL. A broken file is never read as "no limits". - Unknown keys: Ignored.
Example:
{
"schema": 1,
"identity_header": "X-Forwarded-User",
"max_bytes": 10485760,
"allowed_extensions": [
".md",
".pdf"
],
"quota": {
"submissions_per_day": 20,
"bytes_per_day": 52428800
}
}
Size and quota are your deployment's decision, not defaults worth copying blindly.
Tiered settings
Two files at the root hold the settings of the features that read them this way, and every field declares the tier it belongs to:
| Tier | File | Holds |
|---|---|---|
instance |
wikitool.json, committed |
What belongs to the corpus and is the same in every clone |
checkout |
.wikitool-local.json, gitignored |
What is true only for this clone on this machine |
secret |
neither | Tokens and passwords - both files hold only a reference |
Each file is a JSON object of sections, one per feature, beside two keys of its own:
{
"schema": 1,
"instance_id": "6f1c0e52-3d1b-4b8e-9a51-2f0e7d1c9b44",
"<section>": {"<field>": "<value>"}
}
instance_id sits in wikitool.json only. The first wikitool config set creates the file and
the id; the id names the directory of the secrets file below. No feature reads its settings this
way yet, so this version declares no section: config set names the sections it knows, and
doctor reports a section it does not read as a WARN.
One place per value. A value in the file its tier does not name is an error naming the file, the key and the right tier - never an override. The same holds for a key set in both files.
References. A field of tier secret takes only a reference; a field marked private - a host
name, an internal URL - takes a plain value in .wikitool-local.json or a reference in either
file. No other field takes one.
| Reference | Resolves through |
|---|---|
{"ref": "<name>"} - lower-case letters, digits and single _ |
WIKITOOL_SECRET_<NAME>, else the key <name> in the secrets file |
{"ref": "op://<vault>/<item>/<field>"} |
op read, the 1Password CLI the preflight recorded |
The secrets file is ~/.config/chemenu/<instance_id>/secrets.json ($XDG_CONFIG_HOME instead of
~/.config when it is set; %APPDATA%\chemenu\<instance_id>\secrets.json on Windows), a JSON object of
names to strings. Outside Windows it must be readable by its owner only (chmod 600); a file
others can read is refused.
A reference is resolved when a command asks for that value, never when the files are read. A name
nobody provides, an op that is not installed, or an item 1Password does not have ends the command
with exit 1; a 1Password that is locked, signed out or waiting for an approval ends it with exit 42,
for a human to unlock. Setting 1Password up: use-1password-for-settings.md.
Never in the committed file: a field that names a program to run or affects a gate - not even
as a reference. config set does not write such a field either; the user edits
.wikitool-local.json by hand, and doctor checks it afterwards.
wikitool doctor reports one line per tier - settings-instance, settings-checkout,
settings-secrets - and for the references only whether they resolve. docs verify holds a
committed wikitool.json to the placement rules above, a plain secret or private value above
all. Neither file ships with dist export. Why the tiers exist, and what keeping secrets out of
the tree does not protect against: configuration-tiers.md.