Code Review Marker
Track what you have reviewed, directly in the VS Code Explorer.

Right-click a file or a folder, pick a review state, and it colours in. Folders carry down to
everything inside them, roll up whatever is worst below them, and turn green on their own once
you have finished them. If the repo has a scope.txt, everything outside the audit scope greys
out and you get real coverage numbers instead of a guess.
Built for multi-file security reviews, where the hard part is not reading a file — it is
remembering which of three hundred files you have already read.
The model
Two things stack: whether a path is in scope, and what you have decided about it.
|
badge |
meaning |
| (nothing) |
|
In scope, not yet reviewed. This is the default — it is not a state you set. |
| Reviewed — clean |
✓ green |
You read it, it is fine. |
| Revisit |
? yellow |
Something to come back to. |
| Suspicious |
! red |
There is something here. |
| In scope |
(nothing) |
Pulled back into the review by hand, past scope.txt. Looks like any unreviewed file, and counts as one. |
| Out of scope |
(grey label) |
Ruled out by scope.txt, or marked by hand. |
A file changed since you reviewed it gets ✓* in orange — a green file that quietly moved
underneath you is the one thing worse than an unreviewed file.
Folders
Marking a folder marks everything below it, stored as one entry — src/libs/ — not one per
file. Two rules keep that honest:
- A clean folder cannot hide a finding. Mark
src/ clean, then flag one file inside it, and
the folder shows ✓! in red. Folder-level marking is only safe because of this.
- Folders finish themselves. Once every in-scope file below a folder is clean, the folder
turns
✓ without you marking it. Working up a tree, you watch it go green.
To exclude one file from a folder you marked clean, use Clear Review Status on it: it writes
an explicit "not reviewed" that stops the inheritance.
scope.txt
Drop a scope.txt at the repo root and everything not in it greys out. scope.md,
audit-scope.txt and audit-scope.md are picked up too, and matching ignores case, so
SCOPE.md and Scope.txt work as well.
If the client shipped their scope list under some other name entirely, don't rename it —
right-click it in the Explorer and choose 🔍 Review Status → Use as Review Scope. It tells
you straight away how many files it matched.
The parser takes scope files in the shape they usually arrive in — you should not have to
reformat a scope list to use it:
# Audit scope
| Contract | nSLOC |
|-----------------------|------:|
| `src/Vault.sol` | 245 |
- [ ] `src/upgrade/Proxy.sol` 88
* src/libs/ ← a bare directory means everything under it
src/**/*.sol ← globs work
!src/mocks/** ← and negation
Comments (#, //), markdown bullets, checkboxes, backticks, links, table cells and trailing
nSLOC columns are all stripped. Prose lines are skipped rather than turned into a rule.
Resolution is last-match-wins, like a .gitignore read inside out: listing a path puts it in
scope, ! takes it back out, and anything no rule matches is out of scope.
Run Code Review Marker: Scope Diagnostics after adding a scope file. It shows every rule, how it
was read, how many files it matched — and calls out any rule that matched nothing. A typo'd
path in a scope file is invisible otherwise, and it silently drops a contract from the audit.
Without a scope.txt the scope layer is simply off: nothing greys out, and coverage is reported
against a file glob and labelled as an estimate.
The estimate, when there is no scope file
An estimate that counts node_modules, generated bindings and build output is not worth reading,
so the repo's own .gitignore is honoured for it: ignored files are left out of the denominator
and reported as N files hidden by .gitignore in the panel, the report and Scope Diagnostics.
Nested .gitignore files, ! re-inclusion and .git/info/exclude all work the way git reads
them.
Nothing is greyed out by this — the Explorer looks exactly the same — and files you mark by hand
are always counted, .gitignore or not. Two limits worth knowing: a tracked file that matches an
ignore rule is hidden here where git would keep it (we cannot see the git index), and the global
core.excludesFile is not read. Set reviewFlags.scope.respectGitignore to false to count
everything again. With a scope file present, none of this applies — the scope file decides.
Overriding it, one path at a time
A scope file is usually the client's, and editing it to argue with it is the wrong move. So both
directions are available as marks instead: + In Scope pulls a path back in, – Out of Scope takes one out, and Clear Review Status undoes either. They live in
.vscode/review-flags.json alongside your review state; scope.txt is never written to.
+ In Scope on a folder covers everything below it, and those files join the coverage
denominator — the number to watch when you add one. Worth knowing:
- It renders as nothing. The path stops being greyed out and starts counting, which is the
whole point; "in scope" is what every other file already is, so it gets no badge of its own.
- A path is either in scope by hand or carries a review state, never both. Re-marking an
included folder
✓ replaces the include, and the files below it leave the denominator again.
Put the include on the folder and the review marks on the files inside it.
- Folder includes reach only files VS Code searches. Anything hidden by
files.exclude or
search.exclude — node_modules/ by default — has to be added as an individual file.
The exported report lists everything added this way under In scope — added by hand, so the
scope you actually reviewed is never just implied.
The Review Progress panel
In the activity bar, grouped worst-first, with a line at the top saying which scope file is in
force — or that there isn't one, which is the difference between exact coverage and a guess:
Scope: audit-scope.md
▾ ! Suspicious 3
▾ ? Revisit 7
▸ ✓ Reviewed — clean 42
▸ ⬚ Not yet reviewed 66 ← in scope, untouched
▸ – Out of scope 6
▸ ⌫ Missing 1
The activity bar badge shows suspicious + revisit — what you still owe. The view header shows
coverage, 42/118 · 36%.
Not yet reviewed is the group worth having. A list of what you have already read is an audit
trail; the list of what nobody has opened is the one that tells you whether you are done.
Export Review Report writes all of it to a Markdown document — findings with notes, coverage,
the full unreviewed list, and any broken scope rules. It opens unsaved, so nothing lands in your
repo unless you save it.
Where state is stored
.vscode/review-flags.json, one per workspace folder, paths relative to that folder:
{
"version": 1,
"flags": {
"src/libs/": { "state": "clean", "at": "2026-08-10T12:00:00.000Z" },
"src/libs/Math.sol": { "state": "none" },
"src/Vault.sol": { "state": "flagged", "note": "unchecked delegatecall",
"at": "2026-08-10T12:00:00.000Z", "hash": "9f2ad41c8b7e0355" },
"test/": { "state": "skip", "by": "copilot" }
}
}
Sorted keys, two-space indent, one line per path — it diffs cleanly and merges by hand. Commit it
to share review state across a team, or .gitignore it to keep it local. The file is watched, so
pulling a colleague's marks updates the Explorer without a reload, and hand-editing the JSON
works fine.
Keys are workspace-folder-relative POSIX paths. A folder key ends with / — src/libs/ is
the folder, src/libs would be a file called libs. Keys containing .. are dropped on read.
Fields, all optional except state:
| Field |
|
state |
clean · revisit · flagged · include · skip · none — see below. |
note |
Free text, shown in tooltips, the panel and the report. No editing UI yet — write it here. |
by |
Who set the mark, when it wasn't a person. Written by the Copilot tool as copilot; shown in tooltips and called out in the exported report. |
at |
ISO 8601, display only. |
hash |
First 16 hex chars of the SHA-256 of the file's raw bytes — shasum -a 256 <file> \| cut -c1-16. Present, and the mark goes ✓* when the file changes; absent, and it never goes stale. |
none is not "unreviewed". Unreviewed is the absence of an entry. none means "explicitly
not reviewed, and don't inherit from the folder above me" — the only way to punch a hole in a
folder you marked clean. Everywhere else, delete the entry instead.
version must be 1. A file claiming a higher version is displayed but never written to, so
nothing a newer release recorded gets clobbered. The same applies to a file that doesn't parse:
the extension says so, keeps what it could read, and refuses to save until you fix it.
A marked file that disappears is kept, not deleted: switching branches should not destroy
review state. Missing files show up in the panel, where Prune Missing Files removes them
deliberately.
Agents
Copilot
In agent mode the extension contributes one tool, Set Review Status, so Copilot can mark files
as it reviews them. It shows up under Select Tools, and #reviewStatus forces it:
mark everything under src/libs reviewed clean, and flag src/Vault.sol — the delegatecall is unchecked
It writes the same marks the context menu does, hash included, and records "by": "copilot" so its
marks stay distinguishable from yours in the exported report. Requires VS Code 1.99+.
Everything else
Any agent can edit review-flags.json directly — the file is watched, so marks appear as soon as
it writes. .claude/skills/code-review-marker/SKILL.md
is a skill covering the format, where the file lives and the handful of rules that bite (the folder
trailing slash, none, what to do about the hash). Copy that folder into the repo you're
auditing and the agent will pick it up.
An agent can write the scope file too — hand it the client's scope list, then Use as Review
Scope on the result.
Settings
| Setting |
Default |
|
reviewFlags.scopeFile |
scope.txt, scope.md, audit-scope.txt, audit-scope.md |
Candidate scope file names, tried in order; first that exists wins. Use as Review Scope writes this for you. |
reviewFlags.storagePath |
.vscode/review-flags.json |
Where marks are stored. |
reviewFlags.folderRollup |
worst |
worst · own (only explicit marks) · off. |
reviewFlags.outOfScopeBadge |
false |
Add a – badge to greyed files. Off by default — on a large repo the grey alone is much quieter. |
reviewFlags.detectStaleMarks |
true |
Hash files when marked and flag them when they change. |
reviewFlags.showBadges |
true |
Turn off for a colour-only look. |
reviewFlags.panel.maxItems |
2000 |
Cap per group in the panel. |
reviewFlags.scope.fallbackExclude |
build dirs |
Coverage exclusions used only when there is no scope file. |
reviewFlags.scope.respectGitignore |
true |
Leave .gitignored files out of the estimated coverage count. Nothing is greyed out. Used only when there is no scope file. |
Colours are themable — override reviewFlags.clean, .revisit, .flagged, .outOfScope and
.stale under workbench.colorCustomizations.
Decorations need explorer.decorations.colors and explorer.decorations.badges on (both are on
by default). To see marks on editor tabs too, turn on workbench.editor.decorations.colors and
workbench.editor.decorations.badges.
Keyboard
No keybindings ship by default. Every command is registered, so bind what you use:
{ "key": "alt+1", "command": "reviewFlags.mark.clean" },
{ "key": "alt+2", "command": "reviewFlags.mark.revisit" },
{ "key": "alt+3", "command": "reviewFlags.mark.flagged" }
With no Explorer selection these act on the active editor.
Known interaction with git decorations
VS Code merges decorations from every provider. On a file that is both reviewed and modified in
git you will see the badges joined (✓, M), and the label colour goes to whichever provider VS
Code weighs higher — see microsoft/vscode#187756.
Nothing is lost, but the colour on a git-modified file may not be ours.
Development
npm install
npm test # core logic, node:test, no VS Code needed
npm run test:integration # real VS Code host against fixtures/audit
npm run watch # then F5 to launch an Extension Development Host
npx vsce package # -> code-review-marker-0.2.0.vsix
src/core/ has no vscode imports at all — the scope parser and the resolve/rollup logic are
where the edge cases live, and keeping them pure means they are testable without an Electron
host. Everything above it is thin adapters.
fixtures/audit/ is a small repo with a deliberately messy scope.txt, used by the integration
tests and handy as a scratch workspace when developing.