Annotations in the margins of your code that surface during review.
Write [check] comments next to code that needs human judgement
during review. When someone changes the annotated code, marginalia
produces a checklist of things to look at.
These are things no linter or test suite can catch for you:
/* [check] Open the sign-up page on your phone and make sure
/* the button still looks nice on mobile */
.signup-button {
padding: 12px 24px;
border-radius: 8px;
}# [check] Read the generated email aloud.
# Does it still sound welcoming?
def compose_welcome_email(user):
...The syntax is inspired by tagref. Bracket tags that shouldn't collide with any language's comment annotation syntax.
Shows up when code in the enclosing function or block changes.
// [check] Pull up /settings in the browser after changing this.
// The layout breaks if the form gets too wide.
function renderSettingsForm(user) {Shows up whenever anything in the file changes.
-- [check:file] Run this migration against a copy of prod data before merging.
ALTER TABLE users ADD COLUMN ...Shows up when files matching <pattern> change, regardless of where
the annotation lives. Use this when a file should be reviewed in
response to changes elsewhere.
The pattern uses glob syntax:
| Pattern | Matches |
|---|---|
src/**/*.rs |
all .rs files under src/, at any depth |
*.proto |
all .proto files in the repo root |
docs/** |
everything under docs/ |
README.md |
exactly README.md |
src/api/*.py |
.py files directly in src/api/ |
# [check:all email-templates/**] Preview the email templates after changing them.
# They render differently in Outlook.Inspired by tagref. When
code near any [check:tag] or [check:ref] with a given name
changes, marginalia produces a single check listing every location
that shares that name.
Use this to connect pieces of code that must stay in sync but live in different places — an encoder and its decoder, a schema and its migration, a struct definition and its serialisation.
[check:tag <name>] carries a description (on the same line or
continuation comments). [check:ref <name>] is a silent counterpart
that participates in the group but has no description of its own.
Every [check:ref] must have a matching [check:tag] (marginalia
errors otherwise). A [check:tag] can exist alone.
// [check:tag WireFormat] Keep the encoder and decoder in sync.
fn encode(msg: &Message) -> Vec<u8> {
...
}// [check:ref WireFormat]
fn decode(bytes: &[u8]) -> Message {
...
}For files that don't have a comment syntax (images, data files, etc.),
or when you just want cross-file rules in one place, put them in a
.marginalia file at the repo root:
# Lines starting with # are comments.
base: development
when static/logo.png changes:
Open the landing page and check it looks right.
when db/migrations/** changes:
Run the migration against a copy of prod first.
A when <pattern> changes: line starts a rule. Indented lines that
follow are the description. Lines starting with # are comments.
The base: directive sets the base branch to diff against. Without
it, marginalia auto-detects the base branch by checking the remote
default branch (refs/remotes/origin/HEAD), then trying main and
master. If none are found, it falls back to diffing against HEAD
(showing only uncommitted changes). You can always override with
--base <branch> on the command line.
The comments for <pattern>: <prefix> directive teaches marginalia
to recognise line comments in files it doesn't know about. This is
useful for domain-specific formats like Yesod route files:
comments for routes.txt: --
The pattern uses glob syntax. Custom rules take priority over built-in language detection.
For all annotation types except [check:ref], the description
follows the tag on the same line or on subsequent comment lines. No
indentation is required. Blank comment lines are preserved as
newlines. The description ends at a non-comment line.
[check:ref] carries no description — any comments after it are
ignored.
$ marginalia
DO NOT IGNORE THIS MESSAGE
marginalia found the following checks near changed code.
Each check shows what changed, where to look, and what to check.
Reproduce this message by running: marginalia --base main
---
src/components/settings.jsx:12-18 changed (in fn renderSettingsForm)
check src/components/settings.jsx:10-45
Pull up /settings in the browser after changing this.
The layout breaks if the form gets too wide.
---
db/migrations/003_add_column.sql:1-5 changed
check db/migrations/003_add_column.sql entirely
Run this migration against a copy of prod data before merging.
---
src/api/users.py:30-42 changed (matching src/api/**)
check docs/api.html
The API docs are hand-written.
Make sure they still still match the actual endpoints.
---
src/encoder.rs:15-20 changed (tag WireFormat)
check:
src/encoder.rs:10
src/decoder.rs:22
Keep the encoder and decoder in sync.
With nix:
nix build
Or add it as a flake input and use in a pre-commit hook:
# in flake.nix inputs
marginalia.url = "...";
# in pre-commit hooks
marginalia = {
enable = true;
name = "marginalia";
package = marginalia.packages.${system}.default;
entry = "${marginalia.packages.${system}.default}/bin/marginalia";
language = "system";
pass_filenames = false;
verbose = true;
};nix develop
cargo build --release
AGPL-3.0. Everyone can use, modify, and contribute. No one can make a proprietary product out of it. If you modify marginalia and distribute it (or serve it over a network), you must share your changes under the same license.