markgate
CLAUDE.md4 starsChanged 3 months agoMITView on GitHub
markgate is a verification-state cache for hook managers. Users run markgate run -- <cmd> (or the verify / set / clear building blocks) to skip re-running a check when nothing relevant has changed since the last success. This file captures conventions for working on the tool itself; user-facing docs live in README.md and it is the spec — see "README as a spec" below. - cmd/markgate/ — entrypoint; wires cobra into a binary. - internal/cli/ — cobra commands (set, verify,…
- Installs packages
- Commits and pushes
In this repository3 other files it gives its agents.
- audit-before-doneSkill
- iterate-designSkill
- verify-e2eSkill
The file
We split this CLAUDE.md into the rules it gives an agent
9 rules. About 0 are common, 2 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 47
No implicit nesting of
markgate/: when the user gives an explicit path (--state-dir / state_dir:). - Very specific: line 96
Use t.Setenv for env-var coverage (auto-restored).
- Very specific: line 115
Before reporting a task complete, run go test ./...
- Distinctive: line 143
After merging the release PR, confirm it actually shipped rather than assuming: the workflow can succeed while the release is still a draft …
- Distinctive: line 157
No emojis in code, commits, docs, or responses unless the user asks.
- Very specific: line 190
If you spawn an Explore or general-purpose subagent and the task needs a specific branch, pin the branch in the prompt ("on branch …
- Very specific: line 203
Always verify a subagent's negative findings ("X doesn't exist", "no reference to Y") against your current branch with grep before acting — …
- Very specific: line 209
When a commit message contains Japanese or other non-ASCII text, pass it via git commit -F <file> rather than a bash heredoc: heredoc + …
- Very specific: line 274
Always reflects the current source — no go install / make build to keep in sync.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
markgate development notes
markgate is a verification-state cache for hook managers. Users run
markgate run -- <cmd> (or the verify / set / clear building
blocks) to skip re-running a check when nothing relevant has changed
since the last success. This file captures conventions for working on
the tool itself; user-facing docs live in README.md and
it is the spec — see "README as a spec" below.
Layout
cmd/markgate/— entrypoint; wires cobra into a binary.internal/cli/— cobra commands (set,verify,status,clear,run,init,version). Shared wiring is inhelper.go:newGateCtxis the single reconciliation point for flags + env + config. Every command goes through it.addGateFlagsregisters the override flags each command accepts.
internal/config/—.markgate.ymlparsing (Gatestruct,Config.Gate(key)).internal/state/— marker read / write. Atomic save via temp-file + fsync + rename. Callers never touch files directly.internal/hasher/—GitTree,FilesandDiffstrategies implementHasher.internal/gitutil/—git rev-parsewrappers (top-level, git-dir, HEAD SHA), merge-base resolution, and the raw working-tree delta ahash: diffgate digests.internal/key/— key syntax validation ([a-z0-9][a-z0-9-]*).
Design principles
- Zero-config default must keep working. Adding a feature MUST NOT
require editing
.markgate.ymlto preserve existing behavior. Any new override follows the chain: CLI flag > env var >.markgate.yml> default. The canonical wire-up point isnewGateCtxin internal/cli/helper.go; whatever precedence helper an existing override uses is the template for the next one. - Exit codes follow
grep/diff: 0 match, 1 mismatch, 2 error. Errors surface as&ExitError{Code: 2, Err: err}— never panic on user-facing failures. - Atomic writes via temp-file + fsync + rename (see
state.Save). A crash mid-write leaves either the old marker or nothing — never a truncated file. - Relative paths resolve against the repo top-level, never cwd. This keeps hook-invoked commands deterministic regardless of where they run from.
- No implicit nesting of
markgate/when the user gives an explicit path (--state-dir/state_dir:). The user owns the layout they asked for. - A command's preconditions are the ones it actually has. Config
validation lives in
config.Load, and every command that resolves a hasher goes through it.cleardoes not — it deletes a file, and its only config dependency isstate_dir— so it usesconfig.Parseinstead. Requiring a valid document there put the recovery path inside the trap it recovers from: one bad gate made every marker unremovable, including markers for valid gates, with hand-editing YAML as the only way out. Skipping validation for one command is not the same as weakening it; if you add a command that genuinely needs less thannewGateCtxprovides, give it a narrower resolver rather than loosening the shared one. - A digest that cannot change must never read as fresh. An empty
scope hashes to the SHA-256 of nothing — the same constant for
every such gate — so the marker matches forever and the gate can
never block. Two sentinels mark it: a dead
includelist (hasher.ErrDeadScope) and an emptyhash: diffdelta (hasher.ErrDiffBase). Refuse atset, not on the read path.setis where the constant would be recorded, and by then any gated command has had its chance to fill the scope.verifymust stop passing, which exit 1 already achieves; escalating it to exit 2 stopsmarkgate runfrom executing the command that would refill the scope, and a gate on build output betweenmake cleanand the next build is then recoverable only viaclear. That regression shipped once — the test covered bootstrapping with no marker and missed the second build cycle. Refuse only where the configuration is at fault. A live scope this branch happens not to have touched is legitimate and must stay usable. What counts as "live" is per strategy:fileshashes what is on disk,diffdraws from a git delta that can never contain an ignored path, so using the working tree as the universe fordiffis a false all-clear, not a false alarm.
Testing
- End-to-end CLI tests live in
internal/cli/integration_test.go.
They drive the root command via
newRootCmd+root.Execute. Prefer adding tests here for new CLI behavior — internal unit tests tend to re-verify what integration already covers. - Helpers:
initRepo(t)— creates a fresh git repo in a temp dir andt.Chdirs into it (auto-restored).runCmd(t, args...)— invokes the CLI, returns(exitCode, stdout).writeRepoFile(t, dir, rel, body)— writes a file under the repo.
- Use
t.Setenvfor env-var coverage (auto-restored). - For precedence features, test each pair explicitly
(
flag > env,env > config,flag > config) rather than only end-to-end. See theTestStateDir_*cluster as the pattern.
Commands
go build ./... # compile check
go test ./... # full test suite (CLI tests spawn real git repos)
go vet ./... # basic misuse check
make lint # golangci-lint with govet shadow + gocritic +
# staticcheck + gosec + etc. Mirrors CI.
# Runs a pinned version via `go run`, so it needs
# no local install and cannot drift from
# .golangci.yml (which is v2-format: a v1 binary
# exits with a config error and lints nothing).Before reporting a task complete, run go test ./... && make lint.
Plain go vet is not enough: CI's golangci-lint enables shadow
detection, ineffassign, unused-variable, format-string checks, and
more that go vet alone misses. If only go vet passed, CI is your
audit — which is too late.
Releasing (merging to main is not shipping)
tagpr keeps a release PR open against main ("Release for vX.Y.Z"),
accumulating every merge into a CHANGELOG entry. Merging that PR is
what ships: it tags the merge commit, and .github/actions/release
then builds the cross-platform archives, attaches them plus
checksums.txt, takes the GitHub release out of draft, and updates
the Homebrew tap.
Until that happens the work does not exist for anyone consuming markgate — a downstream repo pinned to a released version sees nothing. So a task that exists because a consumer needs a behavior is not done at merge; it is done at release. Treat the release PR as part of the task, not as unrelated repo upkeep. (That framing error is exactly how a release got classified out-of-session once: by its mechanism — "it's a tagpr PR" — rather than by the goal.)
Version: patch by default. The Lint PR workflow adds
minor-release for a feat: title, and .tagpr reads
minorLabels / majorLabels, so a new feature bumps the minor
automatically. Nothing to do by hand unless you want to override.
After merging the release PR, confirm it actually shipped rather than
assuming: the workflow can succeed while the release is still a draft
with no assets. Check isDraft: false and a non-empty asset list,
then download the built archive and smoke it — that is the only test
of the artifact users receive, and it is a different artifact from
the one go test and e2e.sh exercise.
Style
- No comments that describe what the code does. Identifier names do that. Add a comment only when the why is non-obvious (an invariant, a subtle precedence rule, a workaround).
- Match the existing terse comment voice — see
state.Save's description of the temp-file dance. - No emojis in code, commits, docs, or responses unless the user asks.
- Imports grouped stdlib / third-party / internal with blank lines between groups (gofmt + the existing files agree).
README as a spec
The README is the user-facing spec, not marketing. When changing behavior, update the README in the same change. Pay attention to:
- Use cases — keep concrete examples working and honest.
- CLI reference / Per-invocation overrides / Environment variables / Sharing markers — these sections cross-reference. Touch one, audit the others.
- FAQ — likely to contain answers that touch the changed area.
- Link fragments — if you rename a heading, fix every
#heading-slugreference in the file.
Working with Claude on this repo
Two project skills split the workflow by triggering moment:
- iterate-design — fires when the user asks for an opinion or proposes a new flag / env / config field. Sketch options, pick one, hold the line if the user pushes back, get sign-off before coding.
- audit-before-done —
fires when you're about to say "done" / push / create a PR. Covers
implementation discipline, the widened proactive audit, and the
"run what CI runs" rule (
make lint, not justgo vet). Use this before everygit pushon non-trivial work.
Subagents and branches
If you spawn an Explore or general-purpose subagent and the task
needs a specific branch, pin the branch in the prompt ("on branch
feat/foo, check …") and — since the subagent can git checkout in
your working tree — git checkout back to your original branch after
it returns. Otherwise subagent findings can come from the wrong
branch (e.g. "function X doesn't exist" because the agent was on a
branch where X hasn't landed yet).
For review tasks that span multiple branches, or to avoid any risk
of the subagent shifting your working tree, pass
isolation: "worktree" when spawning it. The agent runs against a
throwaway git worktree, so its checkouts / edits never leak back.
Always verify a subagent's negative findings ("X doesn't exist", "no
reference to Y") against your current branch with grep before
acting — the answer is only as good as the branch the agent read.
Commit messages with non-ASCII
When a commit message contains Japanese or other non-ASCII text,
pass it via git commit -F <file> rather than a bash heredoc:
heredoc + $(...) substitution can mis-parse embedded
question-marks or quotes and fail with "unexpected EOF". Writing the
message to a temp file first sidesteps all shell escaping.
Harness (.claude/)
settings.jsonpre-allows read-only dev commands (go test,go vet,git status,gh pr view, ...) to cut permission prompts.hooks/go-vet-on-edit.shis a PostToolUse hook: after every Edit/Write on a.gofile it runsgo vet ./..., and it dogfoods markgate to skip the run when repo state hasn't changed since the last pass. A vet failure exits 2, so the diagnostic is surfaced back to Claude as blocking feedback.hooks/guard-main-branch.shis a PreToolUse hook on Bash: blocksgit commit/git push/git merge/git rebasewhen the current branch ismain. Protects against the "PR merged → local branch still on main (or got switched) → Claude commits to main" mistake, which the project's branch-protection rule catches at push time but only after the commit has already landed locally (expensive to unwind). Exits 2 with a message pointing at the fix (git checkout -b <feature-branch>). Matches the verb anywhere in the command, not just at the start: these almost always arrive inside a compound (git add -A && git commit ...), and a prefix-only match let exactly that through once. Global options are matched too (git -C <dir> commit), since operating on the repo from another cwd is how the mistake happens in the first place. Heredoc bodies are excised first, so writing a script that merely contains a git command is not mistaken for running one —scripts/e2e.shhas eight of them. Quoted strings deliberately are not excised:bash -c "git commit"is a real commit, so a heredoc body (which the shell can never execute) and a quoted string (which it can) are not the same case.hooks/guard-main-branch.test.shis the matrix for the above. Run it after touching that hook:bash .claude/hooks/guard-main-branch.test.sh(exit code = failures). It asserts both directions, because the hook has failed in both — once missing a real commit, once blocking a file write — and each failure is silent in its own way: a miss puts a commit onmain, a false positive teaches the reader to route around the guard. Adding a case to only the direction that last broke is how the other one comes back.hooks/e2e-pre-merge.shis a PreToolUse hook on Bash: before anygh pr merge ...it runs.claude/scripts/e2e.sh(the full black-box CLI smoke), wrapped inmarkgate runso unchanged repos skip in ~0.1s. Failure exits 2 and blocks the merge. The same script is invokable manually via theverify-e2eskill.scripts/e2e.shis the black-box CLI smoke (built binary +git initsandboxes per section). Covers original primitives (set / verify / clear / run / init / version, default key,--hash files,--state-dir, env-var, precedence) and every feature added in the 2026-05-09 batch (completion, config lint, TTL,--explain, bare status, composes / requires) plushash: diff(#68) and the dead-include refusal shared by both scoped hash modes (#70). The script's summary prints the assertion count; exit code = number of failures. Do not restate the count here — it has no way to stay in sync and has already drifted once.
How the dogfood works (no install needed)
The hook invokes markgate via go run ./cmd/markgate rather than a
globally installed binary. Benefits:
- Always reflects the current source — no
go install/make buildto keep in sync. - Go's content-based build cache makes steady-state invocations fast
(~0.1s skip, ~0.3s run after cold compile). Only the first compile
after
go clean -cacheis slow (~2–3s). - Nothing lands outside the repo.
Markers for these hooks live at .git/markgate/hook-vet.json and
.git/markgate/hook-e2e-pre-merge.json (default location, git-
ignored by virtue of being inside .git/).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

