Shipmoor Community CLI is a free local pre-merge check for agent-assisted code. Run it after Claude Code, Codex, Cursor, Copilot, or another coding agent finishes and before you open a PR.
It scans Python, TypeScript, JavaScript, and Go changes, catches high-confidence generated-code failure modes, and emits concise human output, deterministic JSON, SARIF, and stable CI exit codes.
The Community CLI is intentionally local-first:
- No account required.
- No telemetry.
- No source upload.
- No hidden local history database.
- No
sudoinstall requirement. - No enterprise policy, audit, RBAC, or hosted dashboard features.
Current version: 0.4.0. Since v0.3.0 there is one universal shipmoor binary: the free Community scan needs no login, and a local license unlocks the paid IC features (Claim Check, Agent Skills) in the same executable; you never download a different build to upgrade. Community scan output, the JSON / SARIF contracts, and the exit codes are unchanged from the v0.2.x line.
For the design notes behind the human output, see Shipmoor Community CLI v0.2.1: a scan output you can read in five seconds. For the launch story, including live runs against Flask, Zod, and Cobra, see Introducing Shipmoor Community CLI.
First five minutes
Install the CLI:
curl -fsSL https://dl.shipmoor.dev/install.sh | bash
The installer prints the exact export PATH line if ~/.shipmoor/bin is not already on your shell PATH. After exporting, verify:
shipmoor version
# shipmoor 0.4.0
Run shipmoor from inside any repo to see the welcome banner with detected languages, branch, and the suggested next command:
shipmoor
⟨ ◆ ⟩
███████ ██ ██ ██ ██████ ███ ███ ██████ ██████ ██████
██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██
███████ ███████ ██ ██████ ██ ████ ██ ██ ██ ██ ██ ██████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ ██ ██ ██ ██ ██ ██ ██████ ██████ ██ ██
v0.4.0 community · verify agent work before review
───────────────────────────────────────────────────────────────
metaphor · main · 1 changed · ts · js
next shipmoor scan --changed · shipmoor help
From a repository where an agent just made changes, run the zero-config preflight:
shipmoor scan --changed
If Shipmoor reports a finding, write a JSON report and ask for the explanation:
shipmoor scan --changed --json --output shipmoor.json
shipmoor explain <finding-id> --from shipmoor.json
When you want CI or code-scanning evidence, write SARIF:
shipmoor scan --changed --sarif --output shipmoor.sarif
That is the core loop: install, scan changed files, understand the finding, then export evidence when you need it.
Install
Install the CLI with one command:
curl -fsSL https://dl.shipmoor.dev/install.sh | bash
The installer detects macOS or Linux, selects the matching CPU architecture (darwin-arm64, linux-amd64, linux-arm64), verifies the downloaded archive’s SHA-256 against the channel manifest, and installs the executable to:
~/.shipmoor/bin/shipmoor
If that directory is not on your PATH, the installer prints the exact shell export line to add. Verify the install:
shipmoor version
Intel Macs (darwin-amd64) are not supported in this release. The older install-community-cli.sh URL remains supported and installs the same universal binary.
What shipmoor scan --changed shows
Output is designed to answer one question: is this change ready to send for review?
✗ 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.
- Verdict line. One of three states:
✓ Ready(zero findings, green),○ Needs a look(findings, none at or above the gate, amber),✗ Needs work(at least one finding meets the gate, red). The count phrase says it twice on purpose: N of M findings block review. - Context line. What manifests Shipmoor discovered, how many files were scanned, the active gate threshold, and
degraded resolverswhen no manifest was found. On a monorepo with nested manifests (backend/package.json,frontend/requirements.txt) every manifest appears here. - Legend.
⊘ blocks the gate · ○ informational. The glyphs are not decoration; they let the gate impact of any finding be read at a glance. - Findings, grouped by file, blockers first. Files that contain at least one blocker float to the top. Inside each file, blockers come first, then severity order. Every finding is three short lines: the header (
⊘ high :3 phantom import python.phantom_import), the evidence (the offending source quoted in backticks), and the fix (→ ...). - Footer.
✗ gate failor✓ gate pass, with the exit code. Then two→lines naming the exactscanrerun command and the exactshipmoor explain <id>to drill into the first blocker. Finally, a one-line “X medium · Y low won’t block — worth a look” note when non-blocking findings exist.
Colors are intentional. Red marks anything that blocks the gate (the ✗, the ⊘, the critical and high severity words, the gate fail line). Amber marks medium and the ○ Needs a look verdict. Blue marks low. Teal marks the → arrows that introduce a recommendation or a next-step command. Dim gray is metadata. The palette is suppressed automatically when stdout is not a TTY, when NO_COLOR is set, or when --no-color is passed.
A clean scan is short:
✓ Ready - no findings on your change
none detected · 1 file · gate high · degraded resolvers
────────────────────────────────────────────────────────
✓ gate pass · 0 blockers exit 0
Phantom-import subtypes
python.phantom_import, typescript.phantom_dependency, javascript.phantom_dependency, and go.phantom_import classify every finding by subtype:
| Subtype | What it means |
|---|---|
hallucinated_package | No such thing on the package registry. Often a real agent hallucination. |
missing_manifest_entry | The package exists on the registry, but the project did not declare it. |
broken_relative_path | The import is local, but the target file does not exist on disk. |
unresolved_local_module | The import looks local but does not match any module under the project’s source roots. |
The subtype shows up in JSON under finding.subtype and in the human message text, so a reviewer can tell at a glance whether the agent invented something or just forgot to update a manifest.
What Community catches
The full rule list is available with shipmoor rules. Version 0.4.0 ships 30 rules organized into five categories:
- Phantom imports / dependencies. The agent invented a package, mis-spelled one, or used one without declaring it in the project manifest.
python.phantom_import,typescript.phantom_dependency,javascript.phantom_dependency,go.phantom_import. - Placeholder logic.
pass, ellipsis,throw new Error("not implemented"),panic("TODO"), constant returns.*.placeholder.*. - Trust suppression.
any,as any,@ts-ignore,@ts-expect-error. Code that bypasses the type system to make a generated change compile without satisfying the type contract.*.trust.*. - Quality signals. Bare
except, mutable defaults, ignored errors,fmt.Print/console.logleft in production code, large functions.*.quality.*,*.error.*,*.debug.*,*.structure.*. - Control flow. Unreachable code after
returnorthrow.*.control_flow.*.
Severities are aligned across languages by a shared severity policy. See Findings & rules. Hallucinated imports are high in every language. Placeholder panics and not-implemented throws are high. Trust suppressions at public boundaries are medium. Debug output is low.
Scan inputs
Scan a whole repository or a single file:
shipmoor scan .
shipmoor scan src/foo.py
Scan Git changes:
shipmoor scan --changed # staged + unstaged changes
shipmoor scan --staged # staged only (good for pre-commit)
shipmoor scan --diff main...HEAD
Scan an agent-produced patch without applying it:
shipmoor scan --patch agent.patch
--patch reads the unified diff, materializes the changed files in memory (no on-disk write), and runs the same analyzers. When the working directory contains a project manifest, patch mode uses that context to resolve imports, so import React from "react" does not flag as phantom on a normal Next.js patch.
Patch and diff scans parse changed line ranges, scan whole changed files for context, and classify findings by change_status: introduced (this finding landed in the changed lines), modified, preexisting, or unknown.
Output formats
Human-readable output is the default:
shipmoor scan .
Write deterministic JSON:
shipmoor scan . --json --output shipmoor.json
The JSON contract (shipmoor.scan.v1) is stable enough to script around. Key fields per finding: id, rule_id, severity, path, start_line, message, recommendation, evidence, change_status, subtype, and a stable fingerprint hash.
Write SARIF 2.1.0 for GitHub code scanning:
shipmoor scan . --sarif --output shipmoor.sarif
When --json or --sarif writes to stdout, stdout contains only the machine-readable document. Diagnostics go to stderr.
After every JSON or SARIF run that produced at least one finding, Shipmoor prints the exact shipmoor explain command for one finding ID. Copy it to drill in.
CI gate
Use --fail-on to decide when the scan should block:
shipmoor scan . --fail-on critical
shipmoor scan . --fail-on high # default in `.shipmoor.yaml`
shipmoor scan . --fail-on medium
shipmoor scan . --fail-on none
Exit codes are stable:
| Code | Meaning |
|---|---|
0 | Scan succeeded and did not exceed the threshold. |
1 | Scan succeeded and exceeded the threshold. |
2 | Usage or configuration error. |
3 | Unexpected scan failure. |
GitHub Actions
This workflow runs Shipmoor on the merge-base diff, writes SARIF, and uploads it to GitHub code scanning. if: always() makes sure the Security tab is populated even when the gate fails the workflow:
name: Shipmoor
on:
pull_request:
push:
branches: [main]
jobs:
shipmoor:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Shipmoor CLI
run: |
curl -fsSL https://dl.shipmoor.dev/install.sh | bash
echo "$HOME/.shipmoor/bin" >> $GITHUB_PATH
- name: Run Shipmoor
run: |
shipmoor scan \
--diff origin/main...HEAD \
--sarif --output shipmoor.sarif \
--markdown-summary "$GITHUB_STEP_SUMMARY" \
--fail-on high
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: shipmoor.sarif
The three-dot notation in --diff origin/main...HEAD scopes the gate to “what this branch adds relative to the merge base,” which means PRs do not get blocked on legacy findings the branch did not introduce. See GitHub Actions for the shorter composite-action form.
Configuration
Shipmoor looks for .shipmoor.yaml by default, but it can scan without one. Generate a starter config:
shipmoor init
That creates .shipmoor.yaml and adds .shipmoor/ to .gitignore. The default schema:
schema_version: 1
languages:
enabled:
- python
- typescript
- javascript
- go
ignore:
- .shipmoor/
rules:
disabled: []
severity_overrides: {}
thresholds:
fail_on: high
diff:
only_introduced: true
output:
default_format: human
Disable a rule entirely:
rules:
disabled:
- python.placeholder.empty_body
Override severity:
rules:
severity_overrides:
python.quality.mutable_default: high
Scan with an explicit config:
shipmoor scan . --config .shipmoor.yaml
Config precedence is file > command-line flag > built-in default.
Project context and monorepos
The first line of human output reports which manifests Shipmoor is using:
Project context: package.json (28 deps), tsconfig.json. Scanning 30 files.
If a repo has manifests in subdirectories instead of at the root (common in backend/ and frontend/ layouts), Shipmoor walks one level into the scan root and treats each subdirectory with a manifest as its own resolution context. All of them appear in the context line.
If no manifests are detected, the context line includes degraded resolvers:
none detected · 12 files · gate high · degraded resolvers
“Degraded mode” means the resolvers will run, but phantom-import rules will be conservative; they can only flag truly missing imports, not “imported but not declared” cases.
Rule discovery
List available rules:
shipmoor rules
shipmoor rules --json
Explain a finding from a JSON report:
shipmoor explain <finding-id> --from shipmoor.json
explain renders a progressive view: title, location, why, root cause, fix, evidence. The same glyph and color conventions as the scan output apply.
✗ 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
Installer controls
Pin a version:
SHIPMOOR_VERSION=0.4.0 curl -fsSL https://dl.shipmoor.dev/install.sh | bash
Use the staging channel:
SHIPMOOR_CHANNEL=staging curl -fsSL https://dl.shipmoor.dev/install.sh | bash
Install to a custom user-writable path:
SHIPMOOR_INSTALL_DIR="$HOME/bin" curl -fsSL https://dl.shipmoor.dev/install.sh | bash
Once installed, the binary keeps itself current:
shipmoor upgrade
Uninstall:
rm -f ~/.shipmoor/bin/shipmoor
rm -rf ~/.shipmoor/lib/shipmoor
The installer never edits your shell profile silently. It prints the export line and you decide where it lives.
Banner states
shipmoor with no subcommand shows a status banner that adapts to your context:
| State | When it fires | Suggested next command |
|---|---|---|
first_run | Your very first ever shipmoor invocation. | shipmoor scan --changed |
standard | Inside a git repo with uncommitted changes. | shipmoor scan --changed |
clean | Inside a git repo with no uncommitted changes. | shipmoor scan --diff <default-branch>..HEAD |
no_git | Outside a git repo. | shipmoor scan --patch agent.patch |
The banner is suppressed on non-TTY output (CI logs stay uncluttered) and on narrow terminals (compact single-line mode). It also fires once before any subcommand on the very first interactive invocation, so you see a welcome the first time you run any shipmoor command.
What is not in scope
The Community tier deliberately does not do these things; other tools do them better:
- Style. ruff, ESLint, Prettier,
gofmt,golangci-lint. - Vulnerabilities.
pip-audit,npm audit,govulncheck, Dependabot. - SAST. semgrep, CodeQL, Snyk Code.
- IaC scanning. Dockerfile, Kubernetes, and Terraform rules are on the roadmap, not in this release.
- Daemons / file watchers / editor integrations. The Community tier is one binary you run on demand.
- Telemetry, analytics, or accounts. The optional package-registry lookup for the
hallucinated_packagesubtype is the only outbound network call. Disable it withSHIPMOOR_OFFLINE=1.
The paid surfaces build on the same binary and the same local-first model: Claim Check and Agent Skills on the IC plan, the Agent Harness and IDE extension, and shared baselines / team policy on the upcoming Team tier. See Plans & tiers.