Repository navigation
Configuration Reference
The master copy of this page lives in the repo at docs/configuration.md; a test there fails when a setting has no entry.
Waymark reads one YAML file. With no file it runs from environment variables alone (the quick start). Everything here is optional except at least one forge.
- Where the config comes from
- Environment variables
- Command-line flags
-
The config file:
site,include_private,forges,projects,ideas,needs_you,notes,plans,docker,probes,listenanddata_dir - The projects file
- The ideas file
- Notes folders
- Plans and changelogs
- The Docker feed
A complete example is waymark.example.yml.
- The file named by
WAYMARK_CONFIG, if it is set. - Otherwise
/config/waymark.yml, if it exists. In Docker, bind-mount a directory there read-only:-v "$PWD/config:/config:ro". - Otherwise no file: the environment-only quick start.
An unknown key is an error that names the key, and Waymark does not start until the config is valid. Errors never repeat a token.
${VAR} and $VAR in the file are replaced from the environment, so tokens
stay out of it: token: ${GITEA_TOKEN}. A variable that is not set becomes
an empty string.
- Expansion runs on string values only, after the YAML is read: a variable
cannot add keys, and comments are never expanded. A non-string field cannot
take a variable (
heatmap: ${H}fails to load). - In a flow mapping, quote the value:
{token: "${T}"}. Unquoted, YAML reads${T}as the start of another mapping. - There is no escape: a
$followed by a letter, a digit,_,{or one of*#$@!?-always starts a variable ($_xis the variable_x). Keep a value that holds a$in the environment and write${NAME}in the file.
| Variable | Used for | Default |
|---|---|---|
WAYMARK_CONFIG |
Path of the config file |
/config/waymark.yml when it exists |
WAYMARK_LISTEN |
Address to serve on, when the file has no listen
|
:8080 |
DATA_DIR |
State directory, when the file has no data_dir
|
/data |
WAYMARK_TITLE |
Page title, when the file has no site.title
|
Waymark |
TZ |
Time zone, when the file has no site.timezone
|
UTC |
WAYMARK_INCLUDE_PRIVATE |
true or false: show private repos, when the file has no include_private (the only way in the quick start) |
false |
GITEA_URL |
Quick start only: your Gitea or Forgejo address | none |
GITEA_TOKEN |
Quick start only: its read-only token | none |
GITEA_OWNERS |
Quick start only: comma-separated users and orgs to track | the token user's own repos only |
GITHUB_TOKEN |
Quick start only: a read-only GitHub token | none |
GITHUB_OWNERS |
Quick start only: comma-separated users and orgs to track | the token user's own repos only |
The quick-start variables are read only when there is no config file. With a
file, name them yourself: token: ${GITHUB_TOKEN}.
With no config file, Waymark makes up to two forges from the environment:
-
GITEA_URLorGITEA_TOKENset: a forge with idgitea, typegitea, owners fromGITEA_OWNERS, heatmap on. -
GITHUB_TOKENorGITHUB_OWNERSset: a forge with idgithub, typegithub, owners fromGITHUB_OWNERS, heatmap on.
Every other setting takes its default: no projects file, no ideas file, no
notes, no Docker, homepages probed, private repos left out
(WAYMARK_INCLUDE_PRIVATE=true shows them). Each tracked repo becomes a
project; a repo with the same name on both forges is one project, with Gitea
first.
| Flag | What it does |
|---|---|
-listen <addr> |
Serve on this address; wins over listen and WAYMARK_LISTEN
|
-demo |
Serve an invented team from a built-in fake forge: no config, no network, every page says it is demo data |
-dump <path> |
Collect once, write the assembled site JSON to the path and exit |
-fixture <path> |
Serve a saved site JSON file (no sources are read) |
-version |
Print waymark <version> and exit |
At start the log says which config it read (config: /config/waymark.yml,
or config: environment only), and warns of a token whose ${VAR} is
empty or not set; it never prints a value.
Each table lists the key's full path, its type, its default and what it
does. A list key left out takes the default; an empty list ([]) turns it
off.
| Key | Type | Default | What it does |
|---|---|---|---|
site.title |
string |
Waymark (or WAYMARK_TITLE) |
The page title, header and footer |
site.timezone |
string |
$TZ, else UTC
|
An IANA zone such as Europe/Paris. Every date and time on the page is written in it, and "today" and the week boundaries follow it |
site.you |
list of strings | [] |
Your names as notes write them. Only the claude-memory notes type reads it; with no names, nothing in those notes waits on you |
site:
title: Team status
timezone: America/New_York
you: [Mark]| Key | Type | Default | What it does |
|---|---|---|---|
include_private |
bool |
false (or WAYMARK_INCLUDE_PRIVATE) |
false drops private repos everywhere: projects, needs-you, ideas, events, the ship log and the map. true shows them like any other repo |
The page has no login, so private repos are off unless you turn them on.
With true, everything the privacy list
names shows for private repos too (names, issue titles, commit messages),
to anyone who can reach the page: put a login in front of it first.
With false, a repo shows only once it is known to be public:
- A projects-file project shows only once its primary repo is (in its forge's list, or read by name), so while that forge has not answered it stays hidden. A mirror is linked and shown only once it is (its forge's list, a by-name read, or its own view); one whose view says private is not read again. The exception is a mirror on a GitHub forge with no token: GitHub answers 404 to anonymous reads of a private repo, so that mirror is linked as the projects file names it.
- An event shows only when its link is into a repo known to be public, so events stored by an earlier run with private repos stay out, also after a restart before the forges answer.
- A forge's status never names a repo known to be private. A failed read of a repo the projects file names, while its privacy is not known yet, is named (it is your file's name for it).
- A GitHub forge with no token cannot tell a named repo is private (GitHub answers 404 to anonymous reads), so such a project shows only what the projects file says about it.
- The heatmap counts all of the token user's activity, private repos included: a number per day, with no repo names.
A list, in order. The first forge holds the primary repo when the same repo name is on two forges; the others are mirrors (stars, mirror issues, latest release).
| Key | Type | Default | What it does |
|---|---|---|---|
forges[].id |
string | required | Your name for the forge. The projects file uses it as a repo key (- gitea: acme/tool), and it names the forge's source status. Unique, and not one of registry, heatmap, ideas, docker, probes, notes, fdroid
|
forges[].type |
string | required |
gitea, forgejo (the same API) or github. The page names the forge by it (Gitea, Forgejo, GitHub), or Codeberg for one on codeberg.org
|
forges[].url |
string | required for gitea; https://api.github.com for github
|
Gitea or Forgejo: the site address, such as https://git.example.com (Waymark adds /api/v1, so leave it off). GitHub: the API root. An http:// or https:// address with no user name or password (the token goes in token). GitHub Enterprise Server is not supported yet: Waymark sends GraphQL to <url>/graphql, which it serves elsewhere |
forges[].token |
string | none | A read-only token (see Tokens and scopes). Gitea and Forgejo need one: without it their API answers 401 and the forge's source shows as failing. GitHub runs without one but lists nothing: only repos the projects file names are read, and only lightly (the repo, its open issues and its latest release; 60 requests an hour). Its source stays OK, and says why when that leaves owners unlisted or nothing to read. A ${VAR} here that is empty or not set is warned of at start |
forges[].owners |
list of strings | [] |
The users and orgs whose repos are tracked; each repo becomes a project unless the projects file claims or hides it. Empty means the token user's own repos only, on every forge type: not repos the user collaborates on, and not its organisations' repos. List an org or another user to track theirs (and list yourself too, to keep your own). An owner with no repos the token can see is logged once. A repo the projects file names is read even when its owner is not listed |
forges[].heatmap |
bool | true |
Add the token user's contribution days to the ship log's year of commits: Gitea's heatmap, or GitHub's contribution calendar (needs a token). It counts all of that user's activity, private repos included, whatever include_private says: a number per day, no repo names. Turn it off for a forge that mirrors another, or mirrored commits count twice |
forges:
- id: gitea
type: forgejo
url: https://git.example.com
token: ${GITEA_TOKEN}
owners: [acme, acme-labs]
- id: github
type: github
token: ${GITHUB_TOKEN}
owners: [acme]
heatmap: falseWhat a forge reads: the repo list every minute. For each primary repo: its
tags, releases, open issues and PRs (with assignees and review requests),
commits from the last 84 days and Actions runs, again when the repo changes
and at least every 30 minutes; its CHANGELOG.md and plan files when the
repo changes and at least every 6 hours. A mirror gets three reads every 30
minutes: the repo, its open issues and its latest release (and, when a rate
limit stopped those reads, again within a minute of the limit's end).
A refresh (Refresh in the page's command palette, or POST /api/refresh) lists every forge
again; a Gitea or Forgejo forge then reads every primary's detail again,
and a GitHub forge only the repos whose list entry changed or whose last
read failed. A forge that answers that its rate limit is spent (GitHub) is
asked nothing until the reset it names, at most an hour. Its status, and
the row of any other read the limit stops (the heatmap, the filed-idea
search, a projects or ideas file in a repo on it), names the time after
which it will be asked again.
Where the projects file is: a file on disk, or a file
in a forge repo. Set projects.file, or all three of projects.forge,
projects.repo and projects.path; not both.
| Key | Type | Default | What it does |
|---|---|---|---|
projects.file |
string | none | Path of the projects file, such as /config/projects.yaml
|
projects.forge |
string | none | A forges[].id to read the file through |
projects.repo |
string | none |
owner/name of the repo holding it |
projects.path |
string | none | The file's path in that repo, on its default branch |
projects:
file: /config/projects.yaml
# or
projects: {forge: gitea, repo: acme/waymark-config, path: projects.yaml}The file is read again when it changes (a local file's modification time, or
the repo's last update in its forge's list). The last good copy is kept in
data_dir and used when the file cannot be read, so a forge that is down at
start-up does not empty the page. A file that does not parse keeps the last
good copy and says why in the source status.
| Key | Type | Default | What it does |
|---|---|---|---|
ideas.labels |
list of strings | [idea, enhancement] |
Open issues carrying one of these labels (any case) in a project's own repos become ideas. A mirror's issues never do |
ideas.file |
string | none | Path of an optional ideas file |
ideas.forge |
string | none | Or read the ideas file through this forge id |
ideas.repo |
string | none |
owner/name of the repo holding it |
ideas.path |
string | none | Its path in that repo |
ideas.marker |
string | waymark-idea: |
The line "File as issue" puts last in a new issue's body, followed by the idea's id; Waymark searches for it to find issues already filed. It needs a letter or a digit |
ideas:
labels: [idea]
file: /config/ideas.json
marker: "waymark-idea:"Quote the marker: an unquoted value that ends in : is read as a mapping
and the file fails to load. Label ideas come from each repo's 10 newest open
issues, the ones Waymark already reads. Finding filed ideas on GitHub needs a
token (it uses the issue search).
The forge rules for the Needs you list. The other rules (Deploy, Fix, stale PRs, Confirm) are always on; see Needs you.
| Key | Type | Default | What it does |
|---|---|---|---|
needs_you.labels |
list of strings | [needs-you, decision] |
An open issue or PR with one of these labels (any case) is a Decide item. Mirror issues count too |
needs_you.assigned |
bool | true |
An open issue or PR in a primary repo assigned to the token user is a Decide item. Turn it off when one account owns everything |
needs_you.review_requests |
bool | true |
An open PR in a primary repo that asks for the token user's review is a Review item |
Each issue or PR gives one item: a label beats an assignment, which beats a review request.
| Key | Type | Default | What it does |
|---|---|---|---|
notes.type |
string | none |
none hides the notes panel and note events. markdown reads a folder of Markdown files. claude-memory reads a Claude Code memory folder. See Notes folders
|
notes.dir |
string | none | The folder; required for markdown and claude-memory. Mount it read-only |
notes.markers |
list of strings | ["WAITING:", "@me"] |
markdown only: a list item containing one of these, as written and never inside a word, is a Decide item with the marker removed |
notes:
type: markdown
dir: /notes
markers: ["WAITING:", "@me"]| Key | Type | Default | What it does |
|---|---|---|---|
plans.dirs |
list of strings | [docs/plans, docs/superpowers/plans] |
Folders in each primary repo that hold dated plan files. A project's plan_dirs in the projects file overrides it. See Plans and changelogs
|
Set docker.feed or docker.host, not both. With neither, the Docker part
of "Where it runs" and the Deploy rule are off.
| Key | Type | Default | What it does |
|---|---|---|---|
docker.feed |
string | none | Path of a feed file a host cron writes (see The Docker feed), such as /feed/docker.json. Read every 30 seconds; flagged when older than 5 minutes |
docker.host |
string | none | The Docker Engine API: unix:///var/run/docker.sock or tcp://socket-proxy:2375. No credentials, path or query. One GET /containers/json?all=1 every 30 seconds |
docker.label |
string | Docker |
What "Where it runs" calls the host, such as web-01
|
docker:
host: tcp://socket-proxy:2375
label: web-01A project with a containers: list shows exactly those containers. Any
other project gets the container named exactly like its slug and every
container whose org.opencontainers.image.source label is the web page of
one of its repos (primary or mirror). A container named in a project's list
belongs only to that project; any other belongs to the first project that
matches it. The first is the project's main container: the slug one, else a
running one. The running version is the image tag, or the org.opencontainers.image.version
label when the tag is not a version (such as latest).
| Key | Type | Default | What it does |
|---|---|---|---|
probes.homepages |
bool | true |
Also probe each repo's website (homepage) field. A homepage shows its state and 24-hour history in "Where it runs", but never makes a project Live, raises Fix or adds an event |
A homepage comes from repo metadata, which anyone with write access to the
repo can change, so it is only probed at a public address. A name that
resolves to a loopback, private (RFC 1918, IPv6 ULA), link-local, CGNAT
(100.64.0.0/10), unspecified (0.0.0.0/8, ::) or multicast address (IPv4,
IPv6 or IPv4-mapped) is not requested, and shows as "Not probed". Homepage probes do not use HTTP_PROXY or HTTPS_PROXY. The
projects file's urls: probes are yours and are requested as given.
A probe is one GET every 2 minutes with a 5-second limit. A 2xx or 3xx answer (redirects are not followed), 401 or 403 counts as up.
| Key | Type | Default | What it does |
|---|---|---|---|
listen |
string |
WAYMARK_LISTEN, else :8080
|
The address to serve on. -listen wins |
data_dir |
string |
DATA_DIR, else /data
|
Where Waymark keeps state.json (events for 90 days, probe history, each project's last state) and the last good projects and ideas files. The image declares it a volume |
Optional. Every tracked repo becomes a project without it; the file names projects, groups repos, and adds what a forge cannot know. It is identity, never state: nothing in it says how a project is doing. Unknown keys are errors.
owner: acme
categories:
- {key: services, label: Services, icon: server, hue: 150}
- {key: apps, label: Apps, icon: layout-dashboard, hue: 210}
hide_repos: [waymark-config, scratch]
projects:
- slug: beacon
name: Beacon
category: services
icon: bell-ring
summary: Status pages and uptime alerts for small teams.
what: Beacon checks the team's sites every minute and posts incidents to a public status page.
stack: [Go, PostgreSQL]
runs_on: Docker on web-01
repos:
- gitea: beacon
- github: acme/beacon
containers: [beacon, beacon-worker]
urls:
- {label: Live, url: "https://status.example.com", probe: "https://status.example.com/healthz"}
notes: [beacon.md]
relations:
- {to: atlas, kind: monitors}
plan_dirs: [docs/plans]
- slug: inkwell
name: Inkwell
category: apps
repos:
- gitea: inkwell
hold: {kind: parked, since: "2026-09-21", reason: Waiting for the new editor library}| Key | Type | What it does |
|---|---|---|
owner |
string | Owner for repo names written without one: - gitea: beacon means acme/beacon
|
categories[].key |
string | The id projects use in category. unsorted is taken: it is the category of every project without one, and always comes last |
categories[].label |
string | The name shown; defaults to the key |
categories[].icon |
string | An icon name from the built-in set (web/spritegen/icons.txt, Lucide names) |
categories[].hue |
number | The category's colour as a hue, 0 to 360 |
hide_repos |
list of strings | Repo names (no owner, any case, any forge) that never become projects. A repo a project names is still read |
projects[].slug |
string | Required and unique: lower-case letters, digits and dashes. The page is /p/<slug>
|
projects[].name |
string | Shown name; defaults to the slug |
projects[].category |
string | A categories[].key. An unknown key files the project under Unsorted, with a warning in the log |
projects[].icon |
string | Defaults to the category's icon |
projects[].summary |
string | One line for the card |
projects[].what |
string | A paragraph for the project page |
projects[].stack |
list of strings | Languages and tools, shown as tags |
projects[].runs_on |
string | Where it runs, in words, for "Where it runs" |
projects[].repos |
list | One-key entries, - <forge id>: owner/name. The first is the primary (tags, releases, CI, changelog, plans); a repo on another forge is a mirror; more repos on the primary's forge are read in full too |
projects[].containers |
list of strings | Container names for this project, instead of matching by slug or label. A name Docker does not know shows as missing |
projects[].fdroid.index |
string | URL of an F-Droid repo's index-v1.json
|
projects[].fdroid.package |
string | The app's package id in that index. Its published version is the running version for the Deploy rule |
projects[].urls[].label |
string | The link's name, such as Live
|
projects[].urls[].url |
string | The link |
projects[].urls[].probe |
string | A URL to probe every 2 minutes. A down probe raises Fix and an up one makes the project Live. Leave it out for a link only |
projects[].notes |
list of strings | Note file names for this project (both notes types); a listed file that is missing is reported |
projects[].relations[].to |
string | Another project's slug |
projects[].relations[].kind |
string |
depends-on, client-of, fork-of, deploys, sibling, monitors, supersedes, or any word (shown with dashes as spaces). Shown on both projects and drawn on the map |
projects[].hold.kind |
string |
parked or frozen: on purpose, not abandoned |
projects[].hold.since |
string |
YYYY-MM-DD, quoted. A commit after it raises Confirm ("Still parked?") |
projects[].hold.reason |
string | Shown with the hold |
projects[].retired |
bool | Shown as Archived. A repo archived on its forge is Archived too |
projects[].plan_dirs |
list of strings | Plan folders for this project, instead of plans.dirs
|
A repo with the same name as a project's repo on another forge, that no project names, joins that project as a mirror when the project has no repo on that forge. To keep it apart, name it in another project or hide it.
Optional JSON, for ideas that are not issues yet. Each shows on the Ideas
page with "File as issue", a link to a new issue in the project's primary
repo, filled in, whose last line is ideas.marker and the idea's id. Once
filed, the idea links its issue instead.
{
"roundup_date": "2026-10-30",
"ideas": [
{
"id": "beacon-01",
"project": "beacon",
"title": "Status badge for READMEs",
"pitch": "An SVG badge with the current status.",
"why": "Asked for twice this month.",
"size": "S",
"kind": "quick-win",
"existing_issue": 0
}
]
}| Key | Type | What it does |
|---|---|---|
roundup_date |
string |
YYYY-MM-DD the list was written, shown on the Ideas page |
ideas[].id |
string | Required and unique; the filed issue is found by it |
ideas[].project |
string | A project slug. An idea for a project that is not on the page is left out, with a warning |
ideas[].title |
string | The idea, and the new issue's title |
ideas[].pitch |
string | A sentence or two; the issue body starts with it |
ideas[].why |
string | Why it matters; added to the issue body |
ideas[].size |
string |
S, M or L; anything else is Unsized |
ideas[].kind |
string |
quick-win, bold, connective or hardening; anything else is shown as Idea |
ideas[].existing_issue |
number | An issue number in the primary repo that already covers it; no "File as issue" then |
Both types read only inside notes.dir (which may itself be a symlink). A
symlink that leaves the folder, or an absolute symlink, is not followed;
only regular files are read; a file over 1 MiB is skipped and logged once;
a note's body keeps at most 64 KiB. A project shows at most 4 note items in
Needs you.
Any folder of .md files, subfolders included (hidden ones skipped): plain
notes, an Obsidian vault, a docs folder. A file belongs to a project by its
name (<slug>.md, or the slug with - as _), by front matter
project: <slug>, or by the projects file's notes: list. Any of these, so
one file can belong to several projects.
---
project: beacon
updated: 2026-10-30
---
# Beacon
- WAITING: the hosting quote before we move the database
- [ ] a to-do item is never a needs-you item-
Dated by front matter
updated:(2026-10-30,2026-10-30 14:05, or RFC 3339), else the file's modification time. Old notes show their age. -
Decide items: each list item containing a
notes.markersmarker, with the marker removed. Task boxes (- [ ],- [x]) and fenced code never are. A marker right after Markdown punctuation (**@me**,(@me)) does not match. - Shown in the project's Notes panel: the first 40 lines of the body, the rest behind "N more lines".
For a Claude Code memory folder: a MEMORY.md index of links
(- [Beacon](beacon.md): deploy waits on Mark) and one file per note, with
front matter (name, description, modified). Most people want
markdown or none.
- A project's notes are the files its
notes:list names, else<slug with - as _>.md. Only the folder's top level is read. -
Decide items: a clause in the note's
MEMORY.mdline, or in its newest dated heading (## 2026-10-30 ...), that waits on a name insite.you: "awaits Mark", "needs Mark", "for Mark", "Mark's decision" (or call, approval, sign-off, go-ahead, tests). A name matches as written or in capitals, never in lower case, so "we will mark it" is not one. - The panel is called "Claude's notes" and shows each note's index line.
Plans. In each primary repo, Waymark looks in the plan folders for
Markdown files whose names start with a date, 2026-10-28-new-editor.md, or
dated folders holding a plan.md. A plan shows under "Next up" with its
first heading and its checkboxes counted ("3 of 7") while it is partly done
and dated within the last 120 days; at most 3, newest first.
Changelog. A CHANGELOG.md in Keep a Changelog
form gives the project page its ## [Unreleased] items and the last
released version and date.
The feed lets Waymark see containers without the Docker socket.
scripts/docker-feed.sh writes
docker ps -a --format '{{json .}}' to $FEED_DIR/docker.json (default
/var/lib/waymark-feed) by write-and-rename. Run it from the host's cron
every minute:
* * * * * FEED_DIR=/var/lib/waymark-feed /usr/local/bin/docker-feed.shMount the directory, never the single file (a rename detaches a
single-file bind mount), and point docker.feed at the file inside it:
docker run ... -v /var/lib/waymark-feed:/feed:ro ...docker:
feed: /feed/docker.jsonWith docker.host instead, Waymark asks the Engine API directly. A socket
mount gives the container control of Docker even when mounted :ro, so
prefer a socket proxy that allows only container listing (for example
CONTAINERS=1 on a docker-socket-proxy). Mounting the socket itself also
needs its group: --group-add $(stat -c %g /var/run/docker.sock), because
Waymark runs as uid 1000.
Getting started
Using Waymark
Reference