The v0.2.0 launch of Shipmoor Community CLI two weeks ago gave the tool its rule catalog, its scan modes, its CI gate, and its output contracts. What it did not give it was an output you could read.
The old human output was a paragraph and then a list:
Review readiness: Needs work. 5 findings on your change (2 high, 2 medium, 1 low).
Project context: pyproject.toml (8 deps), requirements.txt (12 deps). Scanning 1 files.
Scope: --changed · 1 file
src/billing/webhook.py
Shipmoor scanned 1 files and found 5 findings.
Gate threshold: high
high [would block] src/billing/webhook.py:4 python.phantom_import - Package 'stripe-retry' does not exist on PyPI.
Recommendation: No package named 'stripe-retry' exists on PyPI. Ask the agent to use a real package or remove the import.
high [would block] src/billing/webhook.py:5 python.phantom_import - Package 'requests' is imported but not declared in requirements.txt or pyproject.toml.
Recommendation: 'requests' is used but not declared in requirements.txt or pyproject.toml. Add it or remove the import.
...
It worked. It was honest. It was also a wall of text that did not respect the fact that the person reading it has about five seconds before they switch tabs. If you wanted to know “is this blocked, and what do I type next?” you had to read seven lines to find out.
v0.2.1 fixes that. The scan output is now a one-line verdict, a one-line context, findings grouped by file with the blockers floated to the top, and a footer that names the exact next command. Color is intentional and means something. The explain command grew its own compact view. Nothing about the underlying rules, JSON contract, SARIF emission, or exit codes changed — this is a UX release, end to end.
curl -fsSL https://dl.shipmoor.dev/install-community-cli.sh | bash
shipmoor version
# shipmoor 0.2.1
The shape of the new output
Here is the same scan, against the same fixture, in v0.2.1:
✗ Needs work - 1 of 3 findings block review
none detected · 1 file · gate high · degraded resolvers
⊘ blocks the gate · ○ informational
────────────────────────────────────────────────────────
app.py · 3
⊘ high :3 phantom import python.phantom_import
Local module 'imaginary_shipmoor_package' is referenced but no file matches under PYTHONPATH.
→ 'imaginary_shipmoor_package' looks local but does not resolve from project module paths. Add the file, fix PYTHONPATH, or remove the import.
○ medium :6 empty body python.placeholder.empty_body
`placeholder` - Function 'placeholder' has no meaningful implementation.
→ Either remove the function or implement it before merging.
○ medium :10 mutable default python.quality.mutable_default
`mutable` - Function 'mutable' uses a mutable default argument.
→ Replace `def mutable(items=[])` with `def mutable(items=None)` and initialize inside the function body.
────────────────────────────────────────────────────────
✗ gate fail · 1 high blocks at threshold "high" exit 1
→ fix the 1 blocker, then re-run shipmoor scan tests/fixtures/python_only --fail-on high
→ drill into one shipmoor explain SHM-b00b9982e581ed39
2 medium won't block - worth a look.
The output is five blocks. Each one earns its space.
1. Verdict. One line, one glyph, one phrase. ✓ Ready (no findings), ○ Needs a look (findings, none above the gate), ✗ Needs work (at least one blocker). The count phrase tells you the same thing twice on purpose: N of M findings block review. On the screen the glyph is colored — green check, amber circle, red cross — so peripheral vision is enough to know whether to read further.
2. Context. One line. What manifests Shipmoor found, how many files it scanned, the active gate threshold, and degraded resolvers when no manifest is detected. This is the line that answers “did Shipmoor know what it was looking at?” before you spend time reading findings.
3. Legend. One line. ⊘ blocks the gate · ○ informational. The glyphs are not decoration — they replace the [would block] prefix from v0.2.0 with a single character at the start of every finding, so the gate impact is unambiguous without re-reading the threshold.
4. Findings, grouped by file, blockers first. The old format printed findings in severity order across the whole report, which meant a six-file diff would scatter blockers across the page. v0.2.1 groups by file and floats files with blockers to the top of the report. Inside each file, blockers come first and the rest are ordered by severity. Every finding gets three short lines: the header (⊘ high :3 phantom import python.phantom_import), the evidence (the offending source line in backticks), and the fix (→ Replace ...). The header is one row deep so you can scan a screenful of findings without scrolling.
5. Footer. This is the part that changed the most. The old output ended where the findings ended. The new output tells you:
gate fail · 1 high blocks at threshold "high" exit 1— yes or no, what would block, and what the exit code will be.→ fix the 1 blocker, then re-run shipmoor scan tests/fixtures/python_only --fail-on high— the exact command, with the actual target you scanned, ready to copy.→ drill into one shipmoor explain SHM-b00b9982e581ed39— the ID of the first blocker, paired with the command that explains it. No more “what was the syntax for explain again?”2 medium won't block - worth a look.— a one-line acknowledgement that non-blocking findings exist and are worth coming back to.
The clean-scan version is one paragraph:
✓ Ready - no findings on your change
none detected · 1 file · gate high · degraded resolvers
────────────────────────────────────────────────────────
✓ gate pass · 0 blockers exit 0
That is what you want a passing CI run to look like in a build log. No noise. No “Shipmoor scanned 12 files and found 0 findings.” Just: ready, here is what I knew, exit 0.
Color that means something
v0.2.0 had color support, but everything that was not the severity word was the same gray. In v0.2.1, color carries information.
- Red is reserved for things that block the gate. The verdict glyph on
✗ Needs work, the⊘glyph on each blocker, the severity word forcriticalandhigh, the✗ gate failline in the footer. If you see red anywhere, something blocks. - Amber marks
mediumseverity and the○ Needs a lookverdict — findings that need attention but won’t fail the gate. - Blue is
lowseverity. - Teal marks the
→arrows that introduce a recommendation or a next-step command. The arrow is the thing your eye should land on; the command is what you copy. - Dim gray is metadata: the project context line, file paths in the file-group headers, the
:Nline numbers, the rule IDs in the right margin. It’s there if you need it; it doesn’t compete for attention.
The palette is automatically suppressed when stdout is not a TTY (CI logs, pipes), when NO_COLOR is set, or when --no-color is passed — CI build logs stay readable on grep, and the same scan in your terminal lights up.
A progressive view for shipmoor explain
shipmoor explain used to print the same flat block of fields a JSON dump would: severity, rule, message, root cause, recommendation, location. v0.2.1 reorganizes it into the same grammar as the scan output:
✗ high · phantom import python.phantom_import
app.py:3 · SHM-b00b9982e581ed39 · confidence high · phantom_dependency
────────────────────────────────────────────────────────
why
Local module 'imaginary_shipmoor_package' is referenced but no file matches under PYTHONPATH.
root cause
No matching local module file could be resolved from project paths.
fix
→ 'imaginary_shipmoor_package' looks local but does not resolve from project module paths. Add the file, fix PYTHONPATH, or remove the import.
evidence
import_name: imaginary_shipmoor_package
registry_lookup: not_applicable
Same glyphs, same color rules, same separator. The page reads top-to-bottom: title, location, why, root cause, fix, evidence. If you only have time for the first three lines, you know what the finding is, where it lives, and why the rule fired. If you have more time, you keep reading until you hit the field that answers your question.
The evidence block is sorted and serialized deterministically, so a finding always renders the same way — important when you paste explain output into a PR comment and someone else needs to reproduce what you saw.
What is exactly the same
It is worth being explicit about the lines v0.2.1 did not redraw, because they are the load-bearing parts of the tool:
- The rule catalog. 30 rules, four languages, five categories. Run
shipmoor rulesto list them. No new rules in this release; severities unchanged. - The JSON output (
schema_version: shipmoor.scan.v1). Same fields, same stablefingerprinthashes, samesubtypeaxis on phantom-import findings. A script that consumed v0.2.0 JSON will consume v0.2.1 JSON. - SARIF 2.1.0. Same
partialFingerprints.shipmoorFingerprint, same severity-to-level mapping. The GitHub Actions recipe and the Security tab integration are unchanged. - Exit codes.
0clean,1over threshold,2usage error,3unexpected failure. CI gates that worked yesterday still work today. - Scan modes.
shipmoor scan <path>,--changed,--staged,--diff <range>,--patch <file>. Same five modes, samechange_statusclassification on--diffand--patch. - Monorepo-aware resolution. Nested
package.json/pyproject.toml/requirements.txt/go.modfiles are still discovered and used as resolution contexts. - Patch ↔ changed parity. A
--patchscan of an unapplied diff and a--changedscan of the same change still produce identical fingerprints.
If you are pinning Shipmoor in CI, this is a safe upgrade. The output your humans read got better; the output your machines parse did not move.
The footer is the feature
If there is one piece of this release worth lingering on, it is the footer. It is the answer to a question we kept hearing from people running the tool for the first time: I see the findings. What do I do?
✗ gate fail · 1 high blocks at threshold "high" exit 1
→ fix the 1 blocker, then re-run shipmoor scan tests/fixtures/python_only --fail-on high
→ drill into one shipmoor explain SHM-b00b9982e581ed39
2 medium won't block - worth a look.
The footer is built from the actual run. The rerun command is the actual target you scanned, with the actual --fail-on you used. The explain line names the actual ID of the first blocker. The won't block - worth a look line is suppressed if there are no non-blocking findings. Everything is verbatim copy-paste.
The CLI is, at this point, telling you what to do next. That is the thing we wanted v0.2.0 to do and didn’t quite. v0.2.1 does.
Try it
curl -fsSL https://dl.shipmoor.dev/install-community-cli.sh | bash
cd path/to/your/repo
shipmoor scan --changed
If you have an agent-authored change sitting in your working tree right now, that’s the run we’d most like to hear about. What did Shipmoor catch? What did it miss? What part of the new output saved you a step, and what part still made you squint? The whole point of an MVP release cadence is that the next version is better than this one because the people running it told us what we got wrong.
For the longer story behind the rules, the scan modes, and the CI gate, the v0.2.0 announcement is still the canonical reference: Introducing Shipmoor Community CLI. For the full architecture — every stage, rule, and output, with diagrams — see How the Shipmoor scan works. The getting-started guide has been updated to v0.2.1: Community CLI.
- From the
shipmoor.devEngineering Team with love ❤️