For developers

Embed the verifier

@promptsign/verify puts the PromptSign verifier inside your Node service — the same audited core the CLI runs, wrapped for JavaScript. Check who signed a skill or agent, and whether a byte changed since, without shelling out to a binary or re-implementing the checks.

Why embed it

For marketplaces, registries, and CI that want to answer is this signed by who it claims, and unchanged? question at ingest time, this package wraps the exact Rust verifier the CLI uses. No need to re-implement signature, certificate-chain, and transparency-log checks.

Verify-only. Signing needs a login and a network round-trip, so it stays in the CLI and the browser Sign tool — this package is the read side.

Install

From npm
npm install @promptsign/verify

Prebuilt native binaries are included for common platforms; there is no Rust toolchain to install to use it.

Verify in your app

verify(path) takes a signed directory or file and returns the same VerifyResult the CLI emits with --json. action is the verdict: pass, warn, or fail.

import { verify } from '@promptsign/verify';

// Same result as `promptsign verify --json` — the identical audited Rust core.
const r = verify('./skills/pdf', { noPinUpdates: true });
if (r.action === 'fail') {
  reject('signature invalid or contents changed', r.findings);
} else {
  console.log(`signed by ${r.identity}`);
}

Pass { noPinUpdates: true } from a server. By default, verifying an unseen signer pins that identity to the artifact name (Trust On First Use), so a later swap to a different signer trips a warning. That pin is a durable side effect written under PROMPTSIGN_HOME — useful on a workstation, but in a stateless or concurrent service it means every request tries to mutate shared state and the “first use” is whichever request happened to run first. noPinUpdates keeps verification pure: the same checks run and any existing pins are still enforced, but nothing new is written.

Verify on ingest

The demand-side integration: a directory verifies a submitted bundle before it's listed, and can show “signed by X” honestly. verifyTree(roots) checks several at once; there is also verifyKeyless(bundle) for the certificate-and-log checks alone.

import { verify } from '@promptsign/verify';

// verify-on-ingest: a marketplace checks a submitted bundle before listing it.
export function onSubmit(uploadedDir) {
  const r = verify(uploadedDir);
  return {
    accepted: r.action !== 'fail',
    signer: r.identity,
    issuer: r.issuer,   // present for keyless (Sigstore) signatures
    reasons: r.findings,
  };
}

What you get back

verify and verifyTree return a VerifyResult — the same fields, in the same shape, as promptsign verify --json:

VerifyResult
{
  "target":       "./skills/pdf",              // what you asked to verify
  "policySource": "project (.promptsign/policy.toml)",
  "name":         "pdf",                        // manifest name the signature covers
  "version":      "1.2.0",                      // optional — omitted if unset
  "kind":         "skill",                      // optional — skill | agent | ...
  "identity":     "alice@example.com",          // signer; null when unsigned
  "issuer":       "https://github.com/login/oauth", // keyless OIDC provider; absent for local-key
  "keyid":        "SHA256:…",                   // signing key id; null when unsigned
  "integratedTime": 1783779720,                 // keyless: Rekor witness time (Unix s); absent for local-key
  "signed":       true,                         // a signature was present at all
  "action":       "pass",                       // verdict: "pass" | "warn" | "fail"
  "findings": [                                 // reasons behind a warn/fail
    { "level": "info", "message": "pinned \"pdf\" to identity \"alice@example.com\"" }
  ]
}
action
The verdict, and the only field most callers gate on: pass, warn, or fail. Treat fail as “do not list / do not run.”
signed
Whether any signature was present. A false here with a fail action means unsigned-and-required; distinguish it from a present-but-invalid signature.
identity
Who signed — the string to render in a “signed by” badge. null when unsigned.
issuer
For keyless (Sigstore) signatures, the OIDC provider that vouched for the identity — e.g. GitHub or Google. Absent for local-key signatures.
keyid
The signing key identifier. null when unsigned.
integratedTime
For keyless signatures, the Rekor log integration time (Unix seconds) — the authenticated moment the signature was witnessed, checked against the certificate's validity window. Absent for local-key signatures; it's what the badge renders as the signing time.
name / version / kind
The manifest metadata the signature actually covers. version and kind are omitted when unset.
findings
The reasons behind a warn or fail (each a level + message) — surface these in a tooltip so a rejection is explainable.
target / policySource
What was verified, and which policy decided the verdict — useful in logs and audit trails.

Trust policy and Trust On First Use pins follow the CLI's rules and honour PROMPTSIGN_HOME.

A signature proves who and what — never that an artifact is safe. Compose it with your own review and scanning; see the docs for the trust model.

Show the verdict

To render the result, drop in <VerifyBadge> — a small, dependency-free React component you can copy into your app. It takes a VerifyResult and nothing else: the colour and label are derived from the verdict, so there is no “verified” state a caller can assert by hand. An unsigned or tampered artifact cannot render a green badge.

Signed by alice@example.com via github.comSigned 2026-07-11 14:22 UTCSigned by alice@example.com — warnings via github.comSigned 2026-07-11 14:22 UTCsigner identity is not pinned for "pdf" (trust on first use)Signature invalidcontent changed since signing: skills/pdf/run.py

Hover a badge to see its tooltip — the verified signing time on a passing one, and the findings behind a warn or fail. Verify on the server, then hand the plain result to the client to render:

Using the badge
import { verify } from '@promptsign/verify';
import VerifyBadge from './VerifyBadge';

// server: verify once, hand the plain result to the client
const result = verify(listing.path, { noPinUpdates: true });

// client: colour, text, and the signing time in the tooltip all come from the
// result — there is no "verified" state a caller can pass in by hand
<VerifyBadge result={result} />

The component is yours to copy — grab both files and style them with the four CSS variables (--green / --amber / --red / --gold), or your own:

You don't need to edit the component's CSS to re-skin it. You can theme the badge by defining those variables above it in the DOM — set them on :root to match your brand everywhere:

Theme it to your brand
/* your app's stylesheet — the badge inherits these */
:root {
  --green: #16a34a;   /* pass  */
  --amber: #d97706;   /* warn  */
  --red:   #dc2626;   /* fail  */
  --gold:  #b45309;   /* the signer's name */
}

Because custom properties inherit, set them on a wrapper instead to re-theme only the badges inside that section, leaving the rest of the page alone:

Scope it to one section
/* Because custom properties inherit, set them on a wrapper to
   re-theme only the badges inside it — the rest of the page is untouched. */
.marketplace-panel {
  --green: #0d9488;
  --gold:  #0f766e;
}

// <div className="marketplace-panel"><VerifyBadge result={result} /></div>