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.
Claude Code plugin
The promptsign plugin verifies CLAUDE.md, skills, and agent definitions before Claude Code loads them, and re-checks a skill's bundle before the Skill tool runs it. Sigstore keyless, offline against a trust root pinned in the plugin, nothing phones home.
/plugin marketplace add PromptSign/promptsign-plugin
/plugin install promptsign@promptsignThen run /promptsign:setup once — it reports whether a verifier is present and how to get one if not. Source, hooks, and the full README are on GitHub.
GitHub Actions
Two maintained actions, one to sign and one to verify, so a workflow doesn't have to hand-roll the CLI call or manage the Sigstore trust roots itself. Both install a pinned promptsign release, verify it against the release's checksums, and run on Linux runners only; macOS and Windows runners can call the CLI directly.
Sign in CI
Signs a file or directory using the workflow's own verified identity. No signing key, no secret to store. Requires permissions: id-token: write, since that's what GitHub uses to mint the identity token, and an action cannot grant that permission to itself.
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: PromptSign/promptsign-sign@v1
with:
path: skills/my-skill
version: ${{ github.ref_name }}Doesn't run on pull requests from forks, since GitHub doesn't grant them id-token: write. Sign on merge or on release. Full reference on GitHub.
Verify in CI
Checks that the files at a path still carry a valid signature from the publisher you expect. Offline, no secret, no permissions beyond contents: read, which is why it also runs on pull requests from forks, unlike signing.
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: PromptSign/promptsign-verify@v1
with:
path: skills/vendored-skillAdd identity: https://github.com/OWNER/*/.github/workflows/* to require a specific signer, which also turns an unsigned artifact from a warning into a failure. Full reference on GitHub.
A badge for your README
Add a badge path and the action also writes an SVG rendered from the run it just did, naming the identity it established:
steps:
- uses: actions/checkout@v4
- uses: PromptSign/promptsign-verify@v1
with:
path: skills/
tree: true
badge: .github/promptsign-badge.svgBecause it is generated from a verification rather than composed from a URL, it cannot show green for a release that did not verify — and it is rewritten on failing runs, so a stale green badge cannot outlive the run that earned it. A failure reports verification failed rather than naming a cause, since a failure can equally mean the bytes changed, the signer is not allowed by policy, or a pinned signer changed.
[](https://promptsign.ai/verified)The file lives in your repository and is served by nobody — not us, not a badge service — so no third party learns who reads your README, and it keeps working whatever happens to us. Once you are signing, your artifact can also go in the directory.
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
npm install @promptsign/verifyPrebuilt 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:
{
"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, orfail. Treatfailas “do not list / do not run.” signed- Whether any signature was present. A
falsehere with afailaction means unsigned-and-required; distinguish it from a present-but-invalid signature. identity- Who signed — the string to render in a “signed by” badge.
nullwhen 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.
nullwhen 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.
versionandkindare 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.
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:
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:
/* 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:
/* 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>