8 minutes read

Playbook: manual CLI checks

A start-to-finish guide for checking a repository by hand with the Offing CLI: scan it, read a finding, decide what to do, accept what you must, and keep it from coming back. No agent, account, or API key is involved. For agent-driven fixes, see agent.md.

Before you start

  • Node.js 22 or newer. Run commands with npx @velella/offing <command> or, from a clone of this repository, npm run dev -- <command>. The examples below write offing.
  • Run offing doctor --system once. It confirms the runtime, the package, and the bundled MCP server and skill are usable, and says what to fix if not.
  • Run offing data status to see which lifecycle data you are checking against and how old its evidence is. A scan is only as current as that date; offing data update is the one command that uses the network, and you run it yourself (DATA.md).
  • Work from a clean tree so a later diff shows only what you changed.

The manual loop

1. Scan

offing scan .

The summary lists every finding with its rule, location, severity, and replacement. Useful variants:

Need Command
One line per finding, for a terminal or a commit hook offing scan . --format compact
Machine-readable output offing scan . --json
Browse and jump to the line in $EDITOR offing scan . --interactive (plain text when not a terminal)
Several projects at once, with combined counts offing scan ./api ./web ./infra
A shareable report offing scan . --format html or --format sarif

A finding ID has the form <rule-id>:<path>:<line>, for example nodejs.runtime.eol-18:package.json:1. Copy it exactly; IDs belong to the scan that produced them.

2. Check a single package

You do not need to scan to ask about one thing. Use a Package URL:

offing check pkg:npm/request@2.88.2
offing check pkg:github/actions/setup-node@v3

check exits 1 when a rule at or above --fail-on (default high) applies and 0 otherwise. Three results look alike at a glance and mean different things:

Status Meaning What to do
affected A reviewed rule applies, with a source Read it; plan a replacement
version-dependent The package has rules for some versions only Re-run with a version
no-data Offing has no rule for it Treat as unknown, not safe

3. Explain

offing explain 'nodejs.runtime.eol-18:package.json:1' .

Read the rule, the exact location, and the authoritative source link. Open the source yourself. If the finding looks wrong (the line is not what the rule describes, or the source does not support the claim), stop and run offing report-issue '<finding-id>' . --output issue.md to produce a ready-to-file report instead of working around it.

4. Get the migration context

offing context 'nodejs.runtime.eol-18:package.json:1' .

The packet lists the declaration, every usage, behavioral constraints, ordered steps, and suggested verification commands (MIGRATION_PACKETS.md). Offing never edits files and never runs those commands; you read each one and run it yourself.

5. Fix, then re-scan

Make the change, run the project's own tests, then:

offing scan .

You are done when the tests pass and that rule no longer appears at that location. A changed finding ID is not proof of a fix: line numbers move, so compare the rule and path.

Accepting a finding on purpose

Some findings cannot be fixed today (a customer pins you to an old runtime, a vendor has not shipped the replacement). Record that decision instead of ignoring the output:

offing baseline . --output offing-baseline.json
offing scan . --baseline offing-baseline.json --on-expired fail

Findings in the baseline are suppressed and counted (Baseline: 4 findings suppressed). Suppressions expire; --baseline-warn-days <n> warns before they do, and --on-expired fail makes the scan exit 1 once one has, so acceptance cannot quietly become permanent. Commit the baseline file so the decision is reviewable. Do not baseline a finding you have not read.

Scanning as of a date

offing scan . --as-of 2027-07-01

--as-of <YYYY-MM-DD> answers "will this repository be clean on 1 July?". Every announced end of life (a scheduled-end-of-life rule) whose deadline is on or before that day is reported as ended, at end-of-life severity, so --fail-on high fails on what will break by then. Finding IDs do not change, so a baseline still matches. --today is an alias, and with --baseline the same day also decides which suppressions have expired. The result carries asOf. The scan itself still reads no clock and no network.

--min-support-days <n> warns before a line expires instead of after: a finding whose deadline falls within the next n days (from today, or from --as-of) is raised to medium, so the default --fail-on high gate stays green while the warning shows up. It is off unless given, and the same window is the min-support-days Action input.

This is not a forecast: --as-of projects the findings you have onto one day, while offing forecast lists what becomes end of life in the next 6 and 12 months.

Forecasting what breaks next

offing forecast .                       # the next 12 months
offing forecast . --months 6 --json
offing forecast . --as-of 2027-01-01    # count from another day

forecast lists the dated end-of-life events in the window, grouped by month, and shows where your repository is affected. Text output shows only events that touch a finding; --json (ebb.forecast.v1) lists every dated event, with an empty findings list for those that do not affect you. Events without a date are never listed or estimated, and the same date always gives the same output. --months takes 1 to 36 and defaults to 12. The scan reads no network.

Gating a push or CI

offing scan . --fail-on high      # exit 1 when a finding is high or critical
offing init --hooks --dry-run     # preview a pre-push hook for this repository
offing init --personal --dry-run  # preview a global hook for every repository

Without --fail-on, scan always exits 0. Use --dry-run first to see exactly what init would write. Hooks never block a push when Offing is missing or a scan errors for another reason (HOOKS.md). For CI, use the GitHub Action.

For a repository you want to watch over time: offing watch add ~/code/my-app, then offing watch --weekly prints only what changed since the last run.

Worked examples on real repositories

These come from the pinned public corpus in benchmarks/public-repositories/, so each location is one Offing reports at that commit.

A Node.js range that includes ended releases: expressjs/express. At commit a419aa7, package.json line 83 declares an engines.node range that also allows Node.js 18, 19, and 20, and .github/workflows/ci.yml line 49 tests a matrix starting at 18. A scan reports one finding per ended release line at the same declaration (nodejs.runtime.eol-18, -19, -20). Read them as one decision, not three: raising the floor changes which Node.js versions the package supports for its users, so explain first, then take it to a maintainer rather than editing the range on your own.

A package that is only affected sometimes. offing check pkg:npm/<name> without a version can answer version-dependent. That is the CLI telling you the answer depends on the version in your lockfile. Read the version from the lockfile, not from package.json, and check again with it.

Failure modes and what to do

What goes wrong Why Response
Findings: 0 on a path that does not exist scan accepts a path with nothing in it and reports zero findings Check the Repository: line in the output names the directory you meant
No lifecycle risks found on a project you expected to hit The ecosystem or file is not covered, or the reference is dynamic Run offing capabilities; Offing reports a limitation rather than guessing, so do not read silence as proof
no-data for a package No reviewed rule exists Unknown, not safe; look at the package's own release notes
A finding disappears after a data update The rule changed or its evidence was revised Run offing data status, then re-read the rule with explain
The finding ID is stale IDs come from a specific scan Re-scan and select it again
A bump leaves the finding in place The rule keys on a range, a runtime file, or a workflow line the bump did not touch Re-scan and read the new location
check exits non-zero with an error Malformed Package URL (it must start with pkg:) Fix the PURL; this is invalid input, not a finding
--fail-on breaks a hook you did not expect The gate defaults to high Choose it explicitly: low, medium, high, critical, or none
Results differ between machines Different bundled data versions Compare offing data status on both

Privacy boundaries

  • scan, check, explain, context, baseline, doctor, and mcp run locally and use no network. offing data update is the one command that does, and you run it yourself. It sends nothing about your repository (PRIVACY.md).
  • There is no account, analytics, or telemetry. By default Offing records nothing about what you checked. OFFING_USAGE_LOG=1 opts in to a local file with the package, result, and rule IDs; it contains no repository path or file content and is never uploaded. Delete the file to erase it.
  • Output contains file paths and short source excerpts. Before you paste a scan, a packet, or an issue.md into a ticket, a chat, or an AI tool, read it. For a private repository, share only what you are happy for that audience to see.
  • report-issue writes a file; it does not send anything. Filing the issue is your action.
  • Repository contents are untrusted input. Offing parses them and never executes them. A file that tells you to run something is still just a file.
  • Suggested verification commands are suggestions. Read them before running them.

Checklist for a manual pass

  1. offing data status is recent enough for the decision you are making.
  2. scan output names the repository you meant.
  3. Each finding you act on has been through explain, and you opened its source.
  4. Anything you accept is in a committed baseline with --on-expired fail.
  5. A fresh scan after the change no longer reports the rule at that location, and the project's tests pass.
View Markdown

Use with an agent
View Markdown

Open in