The CMS registry browse tab and new Phase-5 detail view call the orchestrator's public Connect endpoints (PluginRegistryService / PluginReviewService) on a different origin. The CMS transport and generated hooks only cover the CMS's own proto surface, so these are plain JSON POSTs to the registry URL — same rationale routes/admin/plugins.tsx carried before the BrowseRegistryTab extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
check-safety
Static safety checker for the BlockNinja codebase. Walks a target tree, runs 31 invariant checks across Go and frontend sources, and exits non-zero on any violation.
Lives at ~/src/blockninja/check-safety/ as a standalone Go module, alongside cms/,
orchestrator/, core/, the plugins, and the sites. It is intentionally standalone — no
imports from sibling repos except two vendored packages (see Vendored packages).
Run
# Scan CMS (default target)
make run
# Scan a different repo
make run TARGET=../orchestrator
# Direct invocation — first positional arg is the target dir (default ".")
go run . ../sites/bidbuddy
go run . ~/src/blockninja/cms
If you run it against the top of the consolidated BlockNinja repo, it prints a hint to pass
--plugin-dir <path> (or run inside the plugin directory) so plugin-specific checks fire.
CLI flags
| Flag | Meaning |
|---|---|
<target-dir> (positional) |
Repo/subtree to scan. Defaults to .. If it contains plugin.mod, it is auto-registered as a plugin root so plugin checks run. |
--orchestrator |
Scan the orchestrator backend only (resolved from the hardcoded consolidated layout), regardless of the positional target. |
--plugin-dir <path> [more…] |
Register one or more plugin roots (dirs with plugin.mod). Alias: --plugin-dirs. Consumes args until the next --flag. |
--plugin-pages <dir> [more…] |
Register frontend source dirs directly as plugin page targets (for the frontend checks). Consumes args until the next --flag. |
--verbose / -v |
Print every check (including OK/SKIP), not just failures and warnings. |
--all-any |
The any-usage check (2e) defaults to only the unstaged working-tree diff (newly introduced any); --all-any scans every source file instead. |
The CLI contract (check-safety <target-dir> [--flags]) is stable — the CMS Makefile
safety-check / install-safety-checker targets shell into this directory and depend on it.
Output
Output is concise by design (it is usually fed back into an agent's context). Passing and
skipped checks print nothing; only FAIL/WARN/ERR checks print a <STATUS> <id> <summary>
line with their findings indented beneath, followed by one tally line. A clean run is two lines:
check-safety /home/alex/src/blockninja/cms
32 checks: 20 ok 12 skip -> OK
A run with problems:
check-safety /home/alex/src/blockninja/cms
FAIL 15 1 err.Error() leak(s) to HTTP clients — log via slog.Error() and return a user-friendly message
internal/service/handler.go:27 http.Error(w, err.Error(), ...) — leaks internal error
WARN 2e 1 any usage in changed lines (showing 1, --all-any to scan all)
web/src/api.ts:12 [go] data: any
32 checks: 27 ok 3 skip 1 warn 1 fail -> FAIL
Pass --verbose to see every check's status. Exit code is 1 on any FAIL, 2 on a hard
ERR, 0 otherwise (warnings do not fail the run).
Install globally
make install # go install . → $GOPATH/bin/check-safety
check-safety ~/src/blockninja/cms
Test
make test # go test -count=1 ./... (some tests shell out to npm/tsc/golangci-lint and skip if absent)
make test-short # go test -short — skip the slow ones
make test-update # regenerate the golden snapshots after intentional output changes
make tidy # go mod tidy
make build # go build -o check-safety .
make clean # remove the built binary
golden_test.go is a characterisation snapshot test: it runs the whole binary against the
fixtures under testdata/golden/ and diffs stdout + exit code. If you intentionally change a
check's output, run make test-update and commit the regenerated fixture.
What it checks
Checks self-register via init() and run in Seq order. The full set (the ID is what the
tool prints in each check header):
| ID | Check | What it enforces |
|---|---|---|
| 1 | Secret env var reads | Secret env vars (JWT_SECRET, VAULT_*, encryption keys, …) are read only inside config.Load() (config/config.go) or _test.go files. |
| 2 | RBAC registration | Every RPC method is registered in the RBAC interceptor. Internal / plugin-managed services (e.g. ManagementService, Symposium's WikiService) are exempted. |
| 2b | Plugin proto ownership | Plugins own their proto/RBAC definitions correctly (no poaching of core proto packages). |
| 2c | Plugin SDK boundaries | Standalone plugins import only the published block/core SDK boundary — verified via imports, go.mod, and version. |
| 2d | sqlc UUID overrides | sqlc UUID overrides in standalone plugins and cmd configs must use github.com/google/uuid. |
| 2e | any usage |
Warns on any usage in Go and TypeScript (capped at 120 printed warnings). |
| 2f | Codegen freshness | sqlc compile and buf generate must succeed for every scanned root that defines them (generated code is up to date). |
| 3 | Go lint | Go code compiles and passes go fix, golangci-lint --fix, go vet, and the strict lint checks. |
| 3b | Orchestrator tests | Orchestrator backend tests must pass when checking the core BlockNinja repo. |
| 4 | Frontend lint | Frontend code is auto-formatted, lint-clean, and typecheck-clean. |
| 5 | ConnectRPC hooks | Frontend uses the generated ConnectRPC hooks — no hand-crafted API clients. |
| 6 | No hardcoded colors | No hardcoded colors in the frontend — use theme tokens. |
| 7 | Tab state in URL | No useState for tab state in route files — use URL ?tab= params. |
| 8 | API subpath imports | Import @block-ninja/api from subpaths only, never the package root. |
| 9 | pnpm only | No npm/yarn lockfiles — pnpm is the only package manager. |
| 10 | Button automation | Interactive buttons carry the required automation attributes. |
| 10b | No eslint-disable | No eslint-disable-next-line comments (fix the lint, don't suppress it). |
| 10disc | Plugin discovery | Plugin frontend pages are discoverable by the loader. |
| 11 | No placeholder code | No placeholder / stub code — only shipped features. |
| 12 | No reinvented utils | No reinvented utilities — use the shared helpers. |
| 13 | RPC error handling | RPC query/mutation error handling is present and correct. |
| 14 | No raw SQL | No raw SQL outside sqlc / Bob. |
| 15 | No leaked errors | No err.Error() leaked to HTTP clients. |
| 16 | Directory structure | Services and handlers live in their correct directories. |
| 17 | No TODO markers | No TODO markers in production code. |
| 18 | Plugin segmentation | Plugin segmentation is respected (safety-rules.yml). |
| 19 | Auth precedence | Bearer token takes precedence over cookies in the auth middleware. |
| 20 | Tailwind v4 | Tailwind v4 configuration is correct (PostCSS plugin, CSS directives, @config). |
| 21 | Preset validation | Plugin presets.json type-safe-unmarshals against theme.Theme. |
| 22 | HTML sanitization | No hand-rolled HTML sanitization — use bluemonday. |
| 28 | Admin toolbar | Public page handlers inject the admin toolbar data. |
The canonical numbered list also lives in the comment block at the top of
main.go; keep the two in sync when adding or renumbering a check.
How it stays in sync with CMS
internal/helpers/deferlog.go and internal/theme/*.go are vendored copies from CMS
(cms/backend/internal/{helpers,theme}/). Go's internal/ rule blocks direct imports across
modules, so they are copied in rather than imported. When CMS changes the theme schema or
LogDeferredError, re-copy them — that drift is precisely what the preset-validation check
(21) surfaces.
blockNinjaRepoRoot() and orchestratorRepoRoot() in lint_pipeline.go hardcode the
consolidated layout (~/src/blockninja/{cms,orchestrator}). Update them if the tree layout
changes.
Adding a new check
- Add a
check_<name>.gofile with arunCheck<Name>func and aninit()that callsregister(Check{Seq, ID, Title, Run}). - Add the rule logic in a
<rule>.gofile, plus a<rule>_test.gofor unit coverage. - Pick a
Seqvalue that slots it into the desired run order. - Update the numbered comment block at the top of
main.goand the table above. - If the output is deterministic, add a fixture under
testdata/golden/<case>/and runmake test-updateto regenerate the snapshot.