Check that the AI instruction files in a repo, meaning skills, agent
definitions, CLAUDE.md and AGENTS.md, still carry a valid signature from the
publisher you expect, on every pull request.
permissions:
contents: read # that is all, because verifying needs no identity
steps:
- uses: actions/checkout@v4
- uses: PromptSign/promptsign-verify@v1
with:
path: skills/vendored-skillNo id-token, no secret, no network beyond fetching the Sigstore roots. That
means, unlike signing, this runs on pull requests from forks, where the
untrusted change is exactly the thing you want checked.
@v1 follows the latest v1.x release, so fixes arrive without editing your
workflow. It is a moving tag, which means whoever can push to this repository
decides what runs in your CI. Two tighter options, in increasing order of
strength:
- uses: PromptSign/promptsign-verify@v1.0.0 # exact release
- uses: PromptSign/promptsign-verify@8b0e6f2... # v1.0.0 # exact commitA tag can be moved and a commit SHA cannot, so the SHA is the one to use when this check is what stands between a pull request and your main branch. Dependabot and Renovate both update SHA pins and keep the version in the trailing comment current.
As a consumer, gate the third-party skills you vendor. A fork of a popular
skill with three extra lines in SKILL.md is very hard to spot in review, and a
signature check is not fooled by it:
- uses: PromptSign/promptsign-verify@v1
with:
path: .claude/skills
tree: true
identity: https://github.com/obra/*/.github/workflows/*As a publisher, prove your own release still verifies before you ship it.
That is the same gate the promptsign release workflow runs on its own binaries.
Pair it with promptsign-sign:
sign on release, verify on every PR.
| Input | Default | What it does |
|---|---|---|
path |
required | What to verify, one path per line. Files or directories. |
tree |
false |
Walk the paths as roots and report every signable artifact found, including well-known instruction files that are unsigned. Use this for a directory of vendored skills. |
identity |
none | The signer identity signatures must match, glob allowed: https://github.com/OWNER/*/.github/workflows/*. Implies strict, because an unsigned artifact cannot have the required identity. |
issuer |
GitHub's, when identity is set |
The OIDC issuer signatures must come from. Set it for other IdPs. On its own it requires that somebody from that issuer signed, so it implies strict too. |
strict |
false |
Fail on unsigned artifacts too, not only broken ones. |
policy |
none | Your own policy.json. Takes precedence over the three inputs above, which exist only to save you writing one. |
badge |
none | Write a README badge SVG to this path, rendered from this run. See Badge. |
cli-version |
v0.3.0 |
Which promptsign release to install. Pin it. |
| Output | What it is |
|---|---|
result |
Worst outcome across everything checked: pass, warn, or fail. |
failed |
How many artifacts failed verification. |
unsigned |
How many carry no signature at all. |
report |
Path to the full JSON report on the runner. |
badge |
Path to the badge SVG, when badge was set. |
Set badge to a path and the action writes an SVG rendered from the run it just
did, naming the identity it established:
- uses: PromptSign/promptsign-verify@v1
with:
path: skills/
tree: true
badge: .github/promptsign-badge.svg
- name: Keep the badge current
run: |
git add .github/promptsign-badge.svg
git diff --quiet --cached || {
git -c user.name=github-actions -c user.email=github-actions@github.com \
commit -m 'Update the PromptSign badge'
git push
}Then reference it from your README, pointing the link wherever you like:
[](https://promptsign.ai/verified)The file is yours. Nothing is hosted by PromptSign, no image is fetched from us or from a badge service when someone reads your README, and the badge keeps working if this project disappears. It also means no third party learns who is reading your repository.
Three things it deliberately does:
- It names the signer rather than showing a bare checkmark, because signed must never be read as safe. A green badge says these bytes came from that identity, and says nothing at all about what they do.
- It never names a signer on a failure, and never names a cause either. A bundle that fails to verify still carries an identity field, and printing it would credit a publisher the verifier just refused to establish. The badge says "verification failed" rather than "signature invalid", because a failure can equally mean the signer is not allowed by policy or that a pinned signer changed — both of which happen with a perfectly valid signature.
- It is written on failing runs too. Skipping the write would leave the last green badge in place on a repo that no longer verifies, which is worse than showing the failure.
Because it is generated from a real verification rather than composed from a
URL, it is not a claim anyone can paste onto a repo that was never signed. It is
still only as current as the last run, so treat it as a signpost to
promptsign verify, never as a substitute for it.
Fails. The bytes on disk are not what was signed, the signature is invalid, the signer is not allowed by the policy in effect, or a previously pinned signer changed. This is the case worth blocking a merge for.
Warns, by default. The artifact has no signature. Nearly everything in the
ecosystem is unsigned today, so a repo adopting this action should not be blocked
on day one. Set strict: true, or pin an identity, once your own dependencies
are signed and you want the gate to be real.
Annotations land on the Files-changed tab, and every run also writes a job summary listing each artifact, its version, and the identity that signed it.
| Annotation | What it means |
|---|---|
::error on an artifact |
That artifact failed. One per failing artifact. |
::warning on an artifact |
That artifact warned, which today almost always means it is unsigned. |
Process completed with exit code 1 |
GitHub's own annotation for the step that failed. It appears once whenever result is fail, alongside the per-artifact errors. It is not a separate problem, and there is no way to suppress it. |
Error and warning annotations for the same artifact carry the same text. If you run the action twice over one artifact, once strict and once not, the two read as a duplicate even though one is an error and the other a warning.
This repository's own CI shows 2 errors and 1 warning on a green run, and that is the expected state. The
self-checkjob runs the action twice over one deliberately unsigned fixture: once withstrict: true, which produces an error annotation plus the exit-code-1 annotation, and once with the defaults, which produces a warning. The job is green because what it asserts is the verdict of each run, not that each run succeeded. A green badge carrying those three annotations means the action is working.
A signature is not a safety verdict. It proves origin and integrity: these exact bytes, from that identity, unchanged. A signed skill can still be a bad skill. Review and scanning are the safety layer, and they need a stable identity to attach a verdict to, which is what this gives them.
Pin an identity, or the check is weaker than it looks. Without identity,
"signed" only means somebody signed it. Trust-on-first-use cannot help here:
this action always passes --no-pin-updates, because pins written on a throwaway
runner vanish with it, and writing them would silently trust whatever signed
first. In CI, trust is stated up front or not at all.
The action downloads the promptsign release archive and checks it against the
release's SHA256SUMS before running it. Every archive is also published with a
detached PromptSign signature for anyone bootstrapping trust by hand. See the
installation notes.
Trust material, TOFU pins, and the user-level policy are kept under
$RUNNER_TEMP for the duration of the job. A run therefore depends only on the
repository and the inputs, and cannot be decided by a policy.json left in the
home directory of a self-hosted runner.
Linux runners only, on x86_64 or arm64. macOS and Windows runners can call the CLI directly, and the release page has binaries for both.
Pair this with promptsign-signto sign on release and verify on every pull request.
Full docs, including the npm SDK and the Claude Code plugin, are on the Integrate page.
Apache-2.0.