A tiny CLI that signs AI agent configuration files (CLAUDE.md, .cursor/rules,
AGENTS.md, …) and verifies the signatures in CI. An unapproved change to a tracked
config can't land on a protected branch, because the change invalidates a signature that
only a trusted key can reissue.
It's the same idea as a signed lockfile or a signed git commit, but for the instruction files that steer a coding agent with shell and file-write access. It does not judge whether the rules are good — only whether the current content was signed by a key you trust.
Zero dependencies, no build step (plain ESM, Node ≥ 18). Install globally straight from GitHub:
npm install -g github:0xjunger/agentsign
agentsign <command>Or run a single command without installing:
npx github:0xjunger/agentsign <command>Note: the bare name
agentsignon npm is an unrelated package. Install from the GitHub spec above (or from a clone withnode src/cli.js), notnpx agentsign.
# 1. Generate a keypair (do this once, keep the private key off CI).
agentsign keygen --out keys/
# -> keys/agentsign.key (private, PKCS8 PEM — never commit)
# -> keys/agentsign.pub (public — paste into the CI variable)
# 2. After a human reviews the config, sign it.
agentsign sign --key keys/agentsign.key --signer you@example.com
# -> writes agentsign.lock (commit this)
# 3. Verify (this is what CI runs).
AGENTSIGN_PUBKEY="$(cat keys/agentsign.pub)" agentsign verify --pubkey -verify exits 0 if every tracked file matches its signed hash, the signature is valid,
and no untracked config files have appeared. Otherwise it exits 1 with a readable list
of what broke.
| Command | What it does |
|---|---|
agentsign keygen [--out keys/] |
Generate an Ed25519 keypair. |
agentsign sign --key <privkey> [--signer <label>] |
Build + sign agentsign.lock. |
agentsign verify [--pubkey <key|->] |
CI gate: exit 0/1. Reads $AGENTSIGN_PUBKEY when the flag is - or absent. Multiple trusted keys may be comma/space separated. |
agentsign status |
Human-readable summary: signer, signature validity, stale/missing/untracked files. |
sign resolves these globs (override them in .agentsign.json):
CLAUDE.md AGENTS.md GEMINI.md
.cursorrules .cursor/rules/** .windsurfrules
.github/copilot-instructions.md .claude/**
.agentsign.json is itself included in the signed manifest, so the tracked set can't
be quietly narrowed (see Security below).
Copy examples/agent-config.yml into your repo at
.github/workflows/agent-config.yml and make the agent-config check required via
branch protection. That required check is the product — the CLI just powers it.
- run: npx github:0xjunger/agentsign verify --pubkey -
env:
AGENTSIGN_PUBKEY: ${{ vars.AGENTSIGN_PUBKEY }} # from a repo/org variable, NOT the treeAlso commit a .gitattributes with * text=auto (included) so line-ending normalization
doesn't cause false verify failures — agentsign signs raw bytes and leaves normalization
to git.
What verify defends against:
- A tracked config edited without re-signing → hash mismatch.
- A new config file matching the tracked globs but absent from the manifest → untracked.
- Any tampering, backed by a real Ed25519 signature, not a forgeable local secret.
The trust anchor. The public key verify trusts must come from somewhere the PR
author cannot edit — a repo/org variable or secret, never a file in the working tree.
Otherwise an attacker who can edit the config can also swap the key and re-sign. The
private key lives with reviewers; it is never on CI. agentsign stores no trusted keys
in the repo for this reason.
Why .agentsign.json is signed: the untracked-file check re-resolves globs from that file.
If it weren't signed, an attacker could drop a glob and smuggle in a matching config. Since
it's in the manifest, verify recomputes its hash before trusting its globs — any edit
fails as a normal hash mismatch.
Out of scope: a malicious signer (that's key management), instructions in an untrusted third-party repo you cloned, and private-key theft (mitigated later by Sigstore keyless signing).
.agentsign.json— config: the tracked globs. Committed and signed.agentsign.lock— the signed manifest:path → sha256for each tracked file, the signer's public key, and one Ed25519 signature over the canonical manifest. Committed.
node --test