npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fini-proof

v0.6.4

Published

Proof-based code verification: every finding carries a runnable proof, every check has a negative control, and a check that cannot run is NOT_MEASURED, never PASS.

Readme

Fini Proof™ — proof-based code verification for your CI

Docs: https://developer.finipe.com/docs/ · npm: fini-proof · This README describes: version 0.6.4 · Support: [email protected]

Fini Proof checks a code change and gives one verdict: PASS, FAIL or NOT_MEASURED.

  • Every finding shows the file and line, the rule, and a proof command that anyone can run again.
  • Every check has a negative control: a planted defect that the check must catch before it is allowed to judge your code. A check that cannot fail is not evidence.
  • The verdict fails closed: if a check cannot run (unreadable file, missing base branch, no files to check, bad licence), the verdict is NOT_MEASURED. It is never PASS.
  • Every run writes an evidence file (fini-proof/evidence@2: run id, commit and git tree hash, engine versions and hashes, the result of every negative control, input file hashes, who made the change and who verified it, a digest of the whole record) and can write SARIF for GitHub code scanning. See Evidence file.

The checks run on your machine. No telemetry. Without --upload, your code, file names and repository names never leave the machine. What does leave it, and when:

  • Licensed runs: the online plan check. With a licence key, Fini Proof confirms with Finipe that the licence is not revoked and its plan is active: about once every 6 days per seat while it holds a saved confirmation (in its last day; if that attempt fails, at most once every 6 hours); with no usable saved confirmation, for example in a fresh CI runner, every licensed run makes one request. It sends only the licence id, the organisation name, a SHA-256 digest of the licence key (not the key, but it works like a password for this check), a seat hash and the CLI version (details and exact schedule). The Enterprise offline licence makes no network call. The free trial needs a connection for each run (see Licence).
  • --upload (optional, off by default) sends the evidence record (what it contains): file paths and hashes, findings and the commit author's email, never file contents.

Node.js 20 or newer. No dependencies.

Quick start

npx fini-proof check                        # whole repository
npx fini-proof check --base origin/main     # only files changed against main (for pull requests)
npx fini-proof check --format sarif > fini-proof.sarif
npx fini-proof check --fail-on medium       # stricter (default: high)

No install is needed: npx fetches the package from the npm registry. In CI pin the version you have tested, for example npx --yes [email protected] check. npx fini-proof help (or --help) lists every command and flag.

Exit codes: 0 PASS · 1 FAIL · 2 usage error · 3 NOT_MEASURED · 4 licence refused.

The checks (engines)

| Engine | What it catches | Languages | Where it came from | |---|---|---|---| | hollow-tests | Tests that cannot fail: no assertion, only constant assertions (expect(true).toBe(true), assert.ok(true), assert True), swallowed catch, un-awaited promise, done() too early, mock-only / type-only / key-only assertions | JS/TS (Jest, Vitest, Mocha, node:test); Python (pytest, unittest) | Ported from Finipe's F4 vacuous-spec detector (scripts/ci/check-vacuous-spec.mjs) | | secrets | AWS keys, private keys, Slack/Stripe/SendGrid/GitHub/Google/LLM API keys, JWTs in config, real values in .env files, hard-coded passwords | any text file | Ported from Finipe's secrets gate (scripts/ci/check-secrets-and-log-redaction.mjs) | | fail-open | CI steps and scripts that report success when they failed: \|\| true, set +e, continue-on-error, allow_failure, catch { process.exit(0) }, empty catch, except: pass, broken ESM entry-point guards | CI YAML, shell, package.json, JS/TS/Python scripts | Entry-point rule ported from Finipe's check-entrypoint-guard.mjs; the rest generalised | | skip-ratchet | .only / fit (always), and skipped tests above the committed baseline | JS/TS, Python | Skip shapes + SKIP-LEDGER: convention from Finipe F4; ratchet from fini-rule-ratchet.mjs | | tenant-filter | A query on a multi-tenant table with no tenant filter, which leaks data across tenants (IDOR) | Raw SQL in any language; TypeORM, Prisma, Sequelize-style, Knex, Django ORM, SQLAlchemy | Detector shapes from Finipe's .github/scripts/check-tenant-scoping.js; schema-aware table detection is new | | migrations | DROP TABLE / DROP COLUMN, column type changes, ADD COLUMN … NOT NULL without a default, SET NOT NULL, missing or incomplete down / rollback | SQL (plain, Flyway, golang-migrate, goose, dbmate), Prisma, TypeORM, Knex, Alembic, Django | Loop expansion + down() rules from Finipe's scripts/ci/lib/migration-invariants.cjs; destructive-change rules are new |

tenant-filter and migrations are NOT_APPLICABLE (they do not block) in a repository with no migrations or no table that has the tenant column. Each case is recorded with the reason.

Orchestrated scanner: gitleaks

If a gitleaks binary is already on PATH, Fini Proof runs it as the engine gitleaks and holds it to the same rule as its own engines:

  • Before it judges your code, Fini Proof plants three known-shaped secrets (an AWS key, a GitHub token, a private key) in a temporary copy and runs gitleaks on it. If gitleaks misses one, exits with an error, times out or writes a report that is not JSON, the engine is NOT_MEASURED and the run cannot PASS.
  • gitleaks scans a temporary copy of exactly the files Fini Proof hashed (ignore globs, --base and --only apply), with --redact. Findings name the file, line and gitleaks rule; the secret itself is never written anywhere.
  • gitleaks is the primary orchestrated secrets scanner, and the built-in secrets engine ALWAYS runs beside it, in full, with all its own controls, on the same files: it keeps catching what gitleaks cannot see (a key split into string pieces joined by +, an array .join(''), adjacent literals, and shapes a gitleaks release does not match). Each engine has its own status: a missed control or a failed run makes only that engine NOT_MEASURED, and the run cannot PASS. When both report the same occurrence (the same bytes of the same file), it is ONE finding that lists both detections (rule, control and proof command of each); the stronger severity decides whether it blocks. When gitleaks is not installed, the gitleaks engine is NOT_APPLICABLE (with the reason) and the built-in secrets engine is the only secrets scanner.
  • Fini Proof never downloads a scanner. FINI_PROOF_GITLEAKS=<path> uses a binary outside PATH (a path that is not executable is NOT_MEASURED); FINI_PROOF_GITLEAKS=off keeps the built-in engine. The binary's sha256 and version are recorded, and they are part of the engine digest, so a new gitleaks binary counts as a new verifier.
  • gitleaks' generic-api-key rule is entropy based and, on real repositories, matches key-name constants, documentation examples and test values. A hit is ADVISORY: severity low, shown in the text report, JSON, SARIF (a note) and the evidence, and as ADVISORY in state, but it does not fail the run at the default --fail-on high and it never turns a NOT_MEASURED run into a pass. To make it block, use --fail-on low or set { "failOn": "low" } in .fini-proof.json (the flag wins over the file). --fail-on low also blocks the other low findings. Every precise gitleaks rule (AWS key, private key, GitHub token, Stripe, JWT, …) stays high and blocking.

Every ported file carries its source path and Finipe commit in its header.

Other commands

npx fini-proof prove --engine hollow-tests --file test/a.test.js --line 12   # re-run one finding (exit 1 = still there)
npx fini-proof state                                                       # per-property proof state for a run now
npx fini-proof state --against .fini-proof/runs/<id>.json                  # what that run proved, as of the files now
npx fini-proof evidence verify fini-proof-evidence.json                    # re-check an evidence file's digest
npx fini-proof negctl hollow-tests/NC-1                                    # show the planted defect being caught
npx fini-proof negctl --all                                                # all negative controls
npx fini-proof baseline                                                    # accept today's skipped tests; the count can only go down
npx fini-proof usage --export usage.json                                   # local usage meter
npx fini-proof licence                                                     # licence / trial status

A skipped test is allowed without a baseline when a comment names why: // SKIP-LEDGER: JIRA-123 reason.

Proven fixes

npx fini-proof fix              # dry run: prints each finding's outcome, writes .fini-proof/fixes/<run id>.patch
npx fini-proof fix --apply      # the same, and writes the proven fixes to the working tree (never commits)

fix runs the same check, then for each finding does one of three things:

  • fixed (proven) — a deterministic text transform (no AI; nothing about your code is sent anywhere) is applied to a throw-away copy of the repository and proved there by the same engines: the unfixed file still reproduces the finding (red), prove no longer finds it (green), a whole check of that file raises no new finding from any engine, and the rule's negative control is still caught (the check can still fail).
  • manual — no safe transform exists for that rule (or that instance); the engine's own FIX text is shown.
  • refused (fix did not prove) — a transform existed but failed one of the proofs; nothing is changed.

Scope is deliberately small. Fixers exist only for: OR_TRUE (drop || true / || exit 0 / --passWithNoTests), SET_PLUS_E (→ set -e), CONTINUE_ON_ERROR / ALLOW_FAILURE (→ false), CATCH_EXIT_ZERO (→ process.exit(1)), EMPTY_CATCH (rethrow; a swallowing .catch(() => {}) is removed), EXCEPT_PASS (pass → raise, exit(0) → exit(1)) and FOCUSED_TEST (it.only( → it(). Everything else — hollow tests, secrets, tenant filters, every migration rule, skipped tests, entry-point guards, except: return 0 — needs a human decision and stays manual. A proven fix makes the failure visible again; it does not make your tests pass. Review the patch like any other diff.

The evidence record of a fix run carries an optional fixes field (mode, summary, and per finding status, proof { red, finding_gone, no_new_findings, negative_control }, before/after sha256). It is additive, so the schema stays evidence@2 and the digest covers it.

Evidence file (evidence@2)

Every check writes one JSON record (.fini-proof/runs/<run id>.json, or --json-out / --evidence <file>). fini-proof/evidence@2 has everything evidence@1 had, plus:

| Field | What it is | |---|---| | snapshot.tree_hash | git rev-parse HEAD^{tree}. Not a git repository, or no commit yet: null with status: NOT_MEASURED and the reason (the per-file hashes still identify what was checked). With uncommitted changes it is the HEAD tree and dirty: true. | | findings[].property_id | <engine>/<rule>, e.g. hollow-tests/NO_ASSERTION. The unit a proof is about. | | engines[].nc_results[] | { id, expect, caught, ms } for each negative control that ran before the engine was allowed to scan. | | engines[].scope_files | the files the engine read (its scan scope plus schema files it needed). A change to any of them makes its proofs STALE. | | attestation.maker | who made the change: { kind, id, model?, session?, source }. From FINI_PROOF_MAKER_KIND (human or agent), FINI_PROOF_MAKER_ID, FINI_PROOF_MAKER_MODEL, FINI_PROOF_MAKER_SESSION; the Claude Code hook records agent / claude-code with the session; the MCP server records the MCP client's name. Otherwise the git author of HEAD with kind: unknown — the kind is never guessed. | | attestation.verifier | { tool: fini-proof, version, engine_digests, runner: { host_digest, ci } }. The host name is stored only as a digest. |

The digest (evidenceDigest) covers all of this. fini-proof evidence verify <file> re-computes it and fails an edited file, and a record whose maker is fini-proof itself (a verifier cannot attest its own change). Every reader (text, SARIF, state, evidence verify, the PR app) also reads evidence@1 files from older versions.

Proof state

fini-proof state gives every property (<engine>/<rule>) exactly one of six states:

| State | Meaning | |---|---| | PROVEN | The engine passed all its controls, this rule has its own negative control (a planted defect) that was caught and its own positive control (the nearest clean look-alike) that stayed clean, and the rule found nothing in the engine's scope. | | FAILED | The rule found a medium or high finding. Each one has its proof command. | | NOT_MEASURED | Nothing could be established. cause says why: engine-not-applicable (e.g. no migrations in the repository), engine-not-measured, no-scope, control-failed, missing-negative-control or missing-positive-control. | | STALE | The record says PROVEN, FAILED or SELF_REPORTED, but since then a file in its scope was modified, deleted or added (for a whole-repository run), the engine changed (including an external binary such as gitleaks), or the proof context changed: tool version, shared runner, .fini-proof.json, baseline, or any ignore source that builds the scope (.gitignore files, .git/info/exclude, core.excludesFile). A record without a proof context (from an earlier fini-proof) is STALE. Run check again. | | ADVISORY | A low or info rule, such as TENANT_FILTER_UNPROVEN. A recommendation. It never blocks and is never shown as proven. | | SELF_REPORTED | Everything PROVEN needs is in the record, but the record was read from a file that no trusted verifier signed. Its digest shows it is intact, not who ran the check — anyone can edit a record and re-hash it. Never shown as proven. Run fini-proof state (which checks now) instead. |

  • fini-proof state runs the check now (the same licensed path as check, and it writes the same evidence file) and reports the states of that run.
  • fini-proof state --against <evidence.json> runs nothing. It verifies the file's digest (an edited file is refused, exit 3), then compares the sha256 of each file in each proof's scope with the files on disk now, and the recorded proof context with the one now. Without a signature from a trusted verifier key (provenance, none shipped yet), its would-be PROVEN properties are SELF_REPORTED.
  • What a signature proves (provenance@2, 0.5.2). A trusted verifier's signature covers the evidence digest, the run and the placement — { tenantId, repositoryId, businessId } — it vouches for. It is TRUSTED only where the reader's own placement (.fini-proof.json → "placement") equals the signed one; the same record read in another tenant, repository or business is SELF_REPORTED (placement-mismatch). A signature proves who checked this record for this repository; it does not make the record valid anywhere else. provenance@1 signatures (no placement) are checked for integrity but never trusted.
  • Every rule of the six built-in engines and the gitleaks adapter has its own negative and positive control, except skip-ratchet/SKIPPED_TEST: it is never reported as a finding (an over-baseline skip becomes SKIP_RATCHET), so no planted input can produce it. It is info-level and shows as ADVISORY.
  • --format json prints fini-proof/state@2 (@2 added SELF_REPORTED and provenance): { property, state, reason, ncId, runId, snapshot, engineVersion, provenAt, invalidatedBy, … } for each property. state is a report and exits 0; use check to gate.

Upload to your Finipe Developer workspace (optional)

export FINI_PROOF_API_KEY=fpk_…        # Developers → Fini Proof → API keys (shown once)
npx fini-proof check --upload            # repository = origin's owner/name; override with --repository acme/api
  • Two environment variables control the upload. FINI_PROOF_API_KEY is required: without it nothing is sent and a warning is printed. FINI_PROOF_API is optional and sets the upload address; the default is https://developer.finipe.com/api/v1/fini-proof. The address must be https (plain http is accepted for localhost only).

  • The upload is a copy of the evidence file. The checks run on your machine and nothing you upload changes them.

  • Getting a key: sign in to your Finipe Developer workspace at developer.finipe.com, open Developers → Fini Proof, add the repository and create an API key. The key is shown once.

  • The evidence file is sent to POST $FINI_PROOF_API/runs (default https://developer.finipe.com/api/v1/fini-proof; …/repositories/<id>/runs when FINI_PROOF_REPOSITORY_ID or .fini-proof.json placement names the repository) with the repository name, the branch and source (ci when CI is set, else cli). The record carries its own digest, so the receiver can recompute it; the CLI reports only the HTTP result.

  • Exactly what is sent is the evidence record already on disk: the path and sha256 of every checked file and each engine's scope; each finding's file, line, rule, severity, message and proof command (a message can name an identifier such as a table, column or test name; a detected secret appears only as its first 4 and last 2 characters); engine versions, hashes and control results; commit, tree and root-commit hashes; the git author email of HEAD; ignore globs and .fini-proof.json settings; the Node.js version; a digest of the runner host name; the licence or trial status. File contents are never sent.

  • Uploaded records read SELF_REPORTED, not PROVEN, until the hosted verifier key is provisioned and signs them (the shipped trusted keyring is empty; see Proof state).

  • An upload never changes the verdict or the exit code. If it fails (no key, revoked key, network), a warning is printed on stderr and the local evidence file stays the record.

  • The key is sent only in the Authorization header and only over https (plain http is accepted for localhost only). It is never printed.

  • --upload-gzip (or FINI_PROOF_UPLOAD_GZIP=1) sends the body with Content-Encoding: gzip. It is off by default because the workspace must accept it. The evidence is about 156 bytes per checked file (the per-file sha256 and the per-engine scope that STALE needs): measured 1.56 MB for 10,000 files and 3.13 MB for 20,000 files. Gzip makes those 0.47 MB and 0.93 MB. An HTTP 413 answer prints the evidence size and suggests --upload-gzip.

Configuration

.fini-proof.json in the repository root:

{
  "ignore": ["vendor/**", "test/fixtures/**"],
  "tenant": { "column": "tenant_id", "tables": ["orders"], "exemptTables": ["countries"], "exemptPaths": ["src/platform-admin/**"],
              "helpers": ["buildTeamWhereQuery"], "scopeColumns": ["project_id", "user", "token"] }
}
  • helpers: a shared where-builder that adds the tenant condition. Calling it counts as the tenant filter.
  • scopeColumns: other columns your team treats as scoping. Examples are a parent key whose row is itself tenant-scoped (project_id), the signed-in user for per-user data (user), or a secret link token (token). They are recorded in the evidence file.
  • Row level security is exempt from the call-site check only when the schema PROVES it: the table is ENABLEd and FORCEd, no role has BYPASSRLS, and every permissive policy's USING / WITH CHECK binds the tenant column to the session (tenant_id = current_setting('app.tenant_id')::uuid, auth.uid(), …). Such tables are listed as rlsTables. A provably open policy (USING (true), OR true, tenant_id = tenant_id, RLS disabled again) is ignored and the query is checked as usual (MISSING, high). Anything short of proof (no FORCE, no policy, an OR branch, a predicate we cannot read) makes an unfiltered query UNPROVEN with the reason. Both are listed as rlsUnproven in the evidence. An RLS signal alone never makes a table safe. Proof comes only from .sql files that Fini Proof's SQL lexer reads completely (comments, dollar quotes, E-strings, quoted identifiers). A search_path change, dynamic SQL, DDL inside a DO body, a quoted mixed-case table name, or RLS DDL in a non-SQL migration leaves the table unproven. The rule is an allow-list and fails closed. Nothing in the migration set is proven if it contains any of these: a standard_conforming_strings change, a U& escape, an E'' string, DO, EXECUTE, format(), set_config(), a nested comment, a construct the lexer cannot classify, or DDL in a JS/TS/Python migration that is not one static string literal. A statement that weakens RLS in a different file also leaves the table unproven. The exception is when every file involved sits in one migrations directory with numeric or timestamp prefixes of ONE width (001_…, 20240101…_…, Prisma-style directories). Those files are replayed in file-name order, which is the order the runners use. Mixed-width prefixes (9_… next to 10_…) sort differently by name and by number, so they leave the whole set unproven. Every .sql file in scope is parsed, including seeds and fixtures, and a NUL byte in one leaves the set unproven. Every other file that may be executed (scripts, YAML/XML/JSON changelogs, app code, seeds, fixtures and test code, with no directory excluded) is scanned for RLS, policy, role and search_path keyword sequences. Any amount of whitespace, comments or quotes may sit between the words. EVERY text file in scope is read by the same SQL lexer: strings, E'', U&, $$, comments and identifier escapes are decoded, and string contents are lexed again after decoding host-language escapes such as \x65. There is no second raw view. Two tiers follow:
    • Tier A, lexer-confirmed DDL. The lexer places a statement as DROP, CREATE TABLE, ALTER TABLE … RENAME / SET SCHEMA / DISABLE / NO FORCE, a policy statement, SELECT … INTO, CTAS, TRUNCATE, DROP SCHEMA/OWNED, a SUPERUSER/BYPASSRLS role, a search_path change, or an ORM/Liquibase change type naming the table. In a .sql file the statement is replayed exactly. In any other file it is replayed too, but capped: it can make the table UNPROVEN (a low, non-blocking finding by default). It can never prove the table, and it can never break it on its own.
    • Tier B, co-occurrence the lexer cannot place. This is the table name next to a DDL word in prose, in a comment, in text the lexer cannot read, behind an escape that cannot be decoded, or a whole-schema ORM sync or reset. The table is reported unproven and NOT_MEASURED, both in measurements.rlsNotMeasured and in the property's state, and the run verdict is NOT_MEASURED (exit 3). A required check therefore blocks, just as it does for a requested file that was not checked. The exception (0.5.0, F-2) is documentation (.md, .rst, .adoc, .txt, …) that no other file in scope refers to. A mention there stays on the property row as an UNPROVEN note (rlsDocNotes, state NOT_MEASURED with cause documentation-note) and never changes the run verdict. A documentation-type file that something executes, such as a .txt run by psql -f from a Makefile, keeps full weight. It is never a finding: prose never produces one. psql flags before -c / -f (--set ON_ERROR_STOP=1, -v …) do not hide the statement, because every string literal is lexed on its own. DROP SCHEMA, DROP TABLE, SELECT … INTO, CREATE TABLE … AS and a rename into the table's name all re-create the table without RLS. The table stays proven only if ENABLE, FORCE and a policy are established again later in the runners' order. An executable file that cannot be decoded leaves the set unproven. Scanned files are covered inputs of the proof, so if one of them changes, the proof becomes STALE. A DO block counts against a table only if it mentions that table or RLS, policies, roles or dynamic SQL.
  • A query with no visible tenant condition is UNPROVEN (low, non-blocking by default) and not MISSING (high) in three cases, with the reason in the message: it is re-keyed from an already-loaded row (id: invoice.id), the same table was queried with the tenant condition earlier in the function, or the result's tenant column is checked straight after the fetch.

What an RLS proof covers, and what it does not. An rlsTables proof is a statement about the repository text only. It covers the DDL that can be read statically in .sql files, the migration order the runners use, and a keyword scan of every other file that may be executed. It does NOT cover:

  • DDL built at run time in ANY file, seeds and scripts included: string concatenation with variables, a computed table name, an ORM call with a variable name, an admin endpoint. The scans catch many such cases but cannot catch them all, so they are out of scope.
  • DDL run outside this repository: a SQL console, another service, a superuser session.
  • The live database state. Each evidence record that exempts a table says this in measurements.rlsScopeNote. To prove those parts, verify against the database itself.

Adopting on an existing repo: run fini-proof baseline once. It records the migrations already applied (path → sha256). An applied migration that has not changed is history and is not judged again. A new or edited migration is judged. Changing the baseline is a gated change: the CI gate fails a PR that changes it unless a human approves.

Ignored globs are written into the evidence file, so a reviewer can see what was left out. If there is no tenant block, the column is tenant_id (and tenantId), and the tenant tables are the ones whose schema (migrations, Prisma, TypeORM entities, Django/SQLAlchemy models) declares that column. If you set a tenant block and no table matches, the result is NOT_MEASURED, not a pass.

A deliberate exception is written in the code, next to the line, with a reason that stays there for review: // fini-proof: tenant-exempt platform-admin report reads all tenants (SEC-19) and -- fini-proof: allow DROP_COLUMN CHG-812 column unused since release 41.

Add .fini-proof/runs/ to .gitignore. Every check writes an evidence file there.

Claude Desktop (one click, no terminal)

npm run build:mcpb builds dist/fini-proof-<version>.mcpb, a Claude Desktop extension with the CLI bundled inside (no terminal, no npx, works offline). Double-click the file (or Claude Desktop → Settings → Extensions → Install Extension), choose your project folder, and ask Claude: "Run the fini-proof check tool on my project". Fini Proof only reads inside the folder you chose; nothing is uploaded.

AI coding agents (Claude Code, Codex, any MCP client)

The agent that wrote the code does not decide whether it is done.

  • MCP server: npx fini-proof mcp gives the agent three tools: check, explain_finding and list_rules. Claude Code: claude mcp add fini-proof -- npx --yes [email protected] mcp.
  • Claude Code hooks: see integrations/claude-code/. The Stop hook exits 2 with the findings while the verdict is FAIL, so Claude cannot finish. PostToolUse gives feedback on each file as it is written (it does not use a trial run). If the licence or trial refuses the check (trial used up or ended, licence expired or revoked), the Stop hook does not block Claude as if the code were wrong: it exits 0 and shows YOU a message that the work is NOT verified, why, and where to renew. A real FAIL, or a check that could not run, still blocks.
  • Codex and other agents: see integrations/codex/ for an AGENTS.md section and a CI gate. The gate also fails any pull request that changes the verifier's own config without human approval.
  • fini-proof check --only <file> checks just the named files, which is what an editor or agent hook needs. Every named path gets a status in the evidence (scope.requested). A path that is missing, outside the repository, ignored or unreadable makes the verdict NOT_MEASURED, never PASS. A deleted file or a file no engine applies to may PASS; the reason is recorded in scope.emptyReason. Paths are read only inside the repository: ../, absolute paths and symbolic links that leave it are refused before anything is read (also for prove, fix and MCP).

CI

| System | File | |---|---| | GitHub Actions | ci/github-actions-example.yml (pull requests) | | GitLab CI | ci/gitlab-ci.yml | | Jenkins | ci/Jenkinsfile | | Bitbucket Pipelines | ci/bitbucket-pipelines.yml |

Daily run of the default branch. ci/github-actions-daily.yml, ci/gitlab-ci-daily.yml, ci/Jenkinsfile.daily and ci/bitbucket-pipelines-daily.yml check the whole default branch once a day and send the evidence to your Finipe Developer workspace with check --upload (set FINI_PROOF_API_KEY as a CI secret). GitHub and Jenkins carry the schedule in the file; GitLab and Bitbucket need a schedule created once in their UI (the file says where). A failed upload is a warning and never changes the exit code.

Each one is one command: npx --yes [email protected] check --base origin/<target> --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json. Clone with full history (fetch-depth: 0, GIT_DEPTH: 0, depth: full). A shallow clone gives NOT_MEASURED, not a guessed PASS. test/ci-templates.test.mjs runs every one of these commands against a real git repository (a clean pull request must pass, a defective one must fail). The daily templates are run the same way on the default branch, uploading to a local stand-in server.

Licence

  • No use without an active plan or an active trial. A paid licence works until the exact end of its paid period (the licence's signed expiresAt, to the minute; a licence without it ends at 23:59:59 IST of its expiry day), and the 72-hour offline grace of the plan check never lets a run happen after that end. Without a licence, only the free trial below runs.
  • Trial (the CLI trial, granted by Finipe per repository): without a key, Fini Proof runs on 1 repository for 7 days, with at most 7 proof runs. Each trial run needs an internet connection. The CLI asks Finipe for a signed run ticket (it sends the SHA-256 digest of the repository's first commit, a random number and the CLI version, nothing else) and runs only on a ticket that verifies against Finipe's production key, is for this repository and this request, and has not expired. With no answer from Finipe there is no run: no offline trial, no grace. A proof run is one full licensed check (check, state without --against, fix, the MCP check tool, a Claude Code Stop or SubagentStop hook check); prove, negctl, evidence verify, state --against and the Claude Code per-file PostToolUse check do not use one. The per-file check needs no connection, but runs only while the last ticket Finipe signed for the repository has not expired (once the trial has ended it stops too, and it never starts a trial). No account is needed; the trial starts on the first run. ~/.fini-proof/trial.json (or $FINI_PROOF_HOME) is only a display cache of the last ticket: deleting or editing it grants nothing. Every check prints the runs and days left. The 8th run or a run after day 7 is refused (exit 4; the server codes are FINI_PROOF_TRIAL_RUNS_USED and FINI_PROOF_TRIAL_ENDED) with the way to get a licence. fini-proof licence shows the status from the cache without using a run or contacting Finipe. Creating a Finipe account does not reset or restart the CLI trial. The trial request always goes to Finipe's own address; FINI_PROOF_API does not redirect it.
  • Two trials, not one: the CLI trial above covers the checks you run locally and in CI. Separately, the workspace trial of your Developer workspace covers the runs you upload with --upload; it belongs to the workspace (not to a machine), has its own limits, and when it is used up the upload warning says so and gives the link to the plans. Both start from the same published numbers (7 days, 7 proof runs, 1 repository); a licence ends both.
  • Licence key: for a licence key buy a plan at https://finipe.com/fini-proof#pricing or write to [email protected]. A key is signed with Ed25519 and names the organisation, seats, expiry and licence id. Set FINI_PROOF_LICENCE (the key or a path to it), or save it as ~/.fini-proof/licence.key (outside any repository), or as a CI secret. A key file inside a repository (.fini-proof/licence.key) also works, but it is a bearer key: anyone who can read the repository can use your licence, so keep it out of git; the CLI warns on stderr when that file is committed, or not ignored in a repository that has a remote. Pass a key on the command line only as --licence @<file> (a whole key after --licence sits in the process list and your shell history; the CLI hints on stderr when you do). The upload key (fpk_…, for --upload) is a different thing and goes only in FINI_PROOF_API_KEY. In the last two weeks before it expires, every licensed check warns on stderr (licence for <org> expires in N day(s)); that warning never changes a verdict or an exit code. The signature and expiry are checked on the machine; the plan is confirmed online about once every 6 days per seat while it holds a saved confirmation (in its last day; if that attempt fails, at most once every 6 hours); with no usable saved confirmation, for example in a fresh CI runner, every licensed run makes one request (see Online plan check). fini-proof licence shows both, without a network call.
  • Usage meter: every run adds one line to ~/.fini-proof/usage.jsonl (commit authors are stored only as hashes), labelled kind: full or kind: file. fini-proof usage --export writes a summary for billing and seat checks (fini-proof/usage@2): per month and per contributor, full checks separately from per-file editor checks.
  • Fair use (paid plans): 500 full checks per developer per month. A full check is a CLI/CI check (also state, fix), a Claude Code Stop/SubagentStop hook check or the MCP check tool; the Claude Code per-file PostToolUse check is not counted. Going over never blocks your work and is never charged by the CLI — we'll get in touch. (During the trial every full check counts towards its 7 runs; the per-file check does not.)
  • Vendor only: licence issuing and key handling are in LICENCE_OPS.md (npm run admin -- issue …, npm run admin -- keygen --out <dir outside any git checkout>).

Online plan check

A licensed check (check, state without --against, fix, the MCP check tool, a Claude Code hook check) runs only while it holds a confirmation from Finipe, at most 7 days old, that the licence is not revoked and its plan is active.

  • What is sent, and only when a confirmation has to be fetched: POST $FINI_PROOF_API/entitlement (default https://developer.finipe.com/api/v1/fini-proof/entitlement, https only, never through a redirect) with exactly five fields:

    • the licence id and the organisation name;
    • the SHA-256 digest of the licence key. The key itself is not sent, but the digest works like a password for this endpoint: anyone who has it can ask for confirmations for your licence. Keep the licence key, and so its digest, private;
    • a seat hash: SHA-256 of the licence id, your OS user name and the host name. The names are not sent, but someone who can guess them can check a guess against the hash;
    • the CLI version.

    Never code, file names, repository names, commits or author identities.

  • What comes back is a confirmation (an entitlement token) signed with the same Finipe key as licences: licence id, organisation, plan, plan state, seat hash, issue time and an expiry 7 days later. It is kept in ~/.fini-proof/entitlement.json (or $FINI_PROOF_HOME).

  • When the CLI goes online, exactly:

    • For the first 6 days of a confirmation: never. Every check runs on the saved confirmation with no network call.
    • In its last 24 hours: the first licensed check asks for a new one (timeout 5 seconds). With a connection that is one request about every 6 days per seat (per user and machine).
    • If that request fails: the check still runs, and the next attempt is at most once every 6 hours, however many checks you run. The time of the failed attempt is recorded in the same file.
    • Without a usable confirmation (none yet, an edited or untrusted one, or past the grace period): each licensed check asks once, and is refused if that fails.
  • No connection: the check keeps running on the saved confirmation until 72 hours after it expires (a warning names the end of the grace period). After that, or with no saved confirmation, it is refused (exit 4) until the plan check succeeds.

  • Refused (exit 4 at once, and the saved confirmation is deleted): a revoked, expired or unknown licence, an inactive plan, a suspended workspace, a licence presented to another workspace's address, or (only if Finipe has switched seat enforcement on) a licence used by more people than it has seats. There is no grace for a refusal: a licence Finipe has revoked, for example after a refund or a chargeback, stops at the next online check. The message says in plain words what happened and what to do, for example where to renew or whom to write to; it also keeps the server's code in brackets so support can find it. The refusal is about the licence, never a verdict on your code.

  • Licences from a test environment (a staging service signs with its own key) are refused with a message that names the signing key id: this CLI trusts only Finipe's production key.

  • Not accepted: an edited or copied confirmation, or one issued more than 5 minutes in the future by your clock (a clock that is wrong or was set back). Your clock must be roughly right.

  • Payment overdue (PAST_DUE): checks keep running, and each one shows a warning until the payment is settled: on stderr for CLI commands, in the response (text and licenceWarning) for MCP, and as the hook's message for Claude Code hooks.

  • Enterprise offline licence: for air-gapped sites, Finipe can issue a licence marked offline. It makes no network call, ever. It is valid until its expiry, and a revocation cannot reach it.

  • The free trial is the opposite: it needs a connection for each run, because Finipe grants it (see Licence).

  • This check runs in code you install, so it is friction rather than security. What Finipe enforces is on its servers: uploads, the workspace and licence issue check the licence there.

Demo

npm run demo (add -- --pause to stop between steps). See SALES_DEMO.md.

Tests

npm test (node:test, no build). It includes test/site.test.mjs, which checks the developer site (site/) against the code: the CLI reference in site/docs/index.html against fini-proof help, every FINI_PROOF_* variable, the control counts, the rule ids and the trial terms.

For maintainers

This section, Demo and Tests describe the source repository. test/, site/, scripts/, docs/, SALES_DEMO.md and LICENCE_OPS.md are not part of the npm package.

Product decisions: docs/FINI_PROOF_SSOT_2026-09-28.md (repository only). Licence issuing and the developer-site deploy: LICENCE_OPS.md (Finipe only).