Status: Proposed Date: 2026-02-20
AI agent instruction files (SKILLS.md, CLAUDE.md, AGENT.MD) are natural language that the LLM trusts as legitimate instructions. A developer clones a repo or installs a package, the LLM reads the instruction file at session start, and now it's following attacker-controlled directives. This is a supply chain attack that operates at the semantic layer.
Nono's existing sandbox neutralizes the effects of prompt injection — dangerous actions are blocked at the kernel. But there is no mechanism to verify the provenance or integrity of instruction files before the agent ingests them. A tampered SKILLS.md that instructs the agent to take actions within the sandbox's allowed set (e.g., modifying source files in the working directory) is indistinguishable from a legitimate one.
| Attack vector | Example | Current defense |
|---|---|---|
| Tampered instruction file | Attacker modifies SKILLS.md in a cloned repo | None — file is read as-is |
| Malicious dependency | npm/pip package includes a poisoned CLAUDE.md | None — file is in the working directory |
| Typosquatting | Package with a similar name includes malicious AGENT.MD | None |
| Compromised CI/CD | Attacker pushes modified instruction files via compromised pipeline | None |
| Man-in-the-middle | Instruction file modified in transit | None |
Introduce software supply chain provenance for instruction files using Sigstore-compatible cryptographic verification. Files are signed at authoring time, and nono verifies signatures before the agent can read the file. Unsigned or tampered files are hard-denied.
Nono supports two signing modes, both producing Sigstore-compatible bundles:
Keyed signing — Private key stored in the system keystore (via nono's existing keystore module). Suitable for individual developers and local workflows.
nono trust sign SKILLS.md --key <key-id>
-> SHA-256 digest of file content
-> ECDSA P-256 signature over DSSE envelope
-> Output: SKILLS.md.bundle
Keyless signing — Ephemeral key + OIDC identity + Fulcio certificate + Rekor transparency log. Suitable for CI/CD pipelines and organizations. The signer's identity is cryptographically bound to the signature via the OIDC token.
nono trust sign SKILLS.md --keyless
-> SHA-256 digest of file content
-> Request OIDC token (GitHub Actions ambient, or interactive OAuth)
-> Fulcio issues short-lived certificate binding OIDC identity to ephemeral key
-> ECDSA P-256 signature over DSSE envelope
-> Rekor logs the signature for transparency
-> Output: SKILLS.md.bundle (contains signature + certificate + Rekor inclusion proof)
Signing is also supported via cosign sign-blob for environments that already have cosign installed. The nono binary provides native signing and verification so that cosign is not a runtime dependency.
Signatures use the DSSE (Dead Simple Signing Envelope) format wrapping an in-toto attestation statement. This is the same format used by SLSA provenance.
DSSE envelope:
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64url of Statement>",
"signatures": [
{
"keyid": "",
"sig": "<base64url signature over PAE(payloadType, payload)>"
}
]
}In-toto Statement (the payload):
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "SKILLS.md",
"digest": {
"sha256": "a1b2c3d4e5f6..."
}
}
],
"predicateType": "https://nono.dev/attestation/instruction-file/v1",
"predicate": {
"version": 1,
"signer": {
"kind": "keyed",
"key_id": "nono-keystore:default"
}
}
}For keyless signing, the predicate.signer section carries the OIDC provenance:
{
"signer": {
"kind": "keyless",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:org/repo:ref:refs/heads/main",
"repository": "org/repo",
"workflow_ref": ".github/workflows/sign-skills.yml@refs/heads/main"
}
}Sigstore bundle: The DSSE envelope is wrapped in a Sigstore bundle (v0.3) which additionally contains the Fulcio certificate chain and Rekor inclusion proof, making the bundle fully self-contained for offline verification.
Storage: Bundles are stored as <filename>.bundle alongside the signed file (e.g., SKILLS.md.bundle). This is git-friendly and follows cosign conventions.
project/
SKILLS.md
SKILLS.md.bundle # Sigstore bundle (DSSE + cert + Rekor proof)
.claude/
commands/
deploy.md
deploy.md.bundle
CLAUDE.md
CLAUDE.md.bundle
Bundles are checked into version control alongside the files they attest. A missing bundle for a file that matches instruction file patterns triggers verification failure.
Trust policy is defined in a dedicated JSON file, separate from the sandbox policy (policy.json). This keeps concerns separated: policy.json defines sandbox rules, trust-policy.json defines attestation trust.
{
"version": 1,
"instruction_patterns": [
"SKILLS*",
"CLAUDE*",
"AGENT.MD",
".claude/**/*.md"
],
"publishers": [
{
"name": "anthropic-official",
"issuer": "https://token.actions.githubusercontent.com",
"repository": "anthropics/claude-skills",
"workflow": ".github/workflows/sign-skills.yml",
"ref_pattern": "refs/tags/v*"
},
{
"name": "my-org",
"issuer": "https://token.actions.githubusercontent.com",
"repository": "my-org/*",
"workflow": "*",
"ref_pattern": "*"
},
{
"name": "local-dev",
"key_id": "nono-keystore:default"
}
],
"blocklist": {
"digests": [
{
"sha256": "deadbeef...",
"description": "Known malicious SKILLS.md variant",
"added": "2026-02-20"
}
]
},
"enforcement": "deny",
"override_flag": "--trust-override"
}instruction_patterns — Glob patterns that identify instruction files. Any file matching these patterns is subject to attestation verification. This applies regardless of directory depth — a SKILLS.md in a subdirectory or dependency is caught.
publishers — Trusted publisher identities. Each entry defines an acceptable signer. For keyless signing, the publisher identity is extracted from the Fulcio certificate's OIDC extensions. For keyed signing, it matches the key ID. A file's signature must match at least one publisher entry to be trusted.
Publisher fields for keyless (OIDC) publishers:
| Field | Description | Supports wildcards |
|---|---|---|
name |
Human-readable identifier | No |
issuer |
OIDC issuer URL (e.g., GitHub Actions token endpoint) | No |
repository |
Source repository (e.g., org/repo) |
Yes (org/*) |
workflow |
Workflow file that signed (e.g., .github/workflows/sign.yml) |
Yes (*) |
ref_pattern |
Git ref pattern (e.g., refs/tags/v*, refs/heads/main) |
Yes |
Publisher fields for keyed publishers:
| Field | Description |
|---|---|
name |
Human-readable identifier |
key_id |
Key identifier in the keystore (e.g., nono-keystore:default) |
blocklist — Known-malicious file digests. Checked before any other verification. A file matching a blocklist digest is hard-denied regardless of signature validity. The blocklist can be extended over time (community-maintained, fetched from a remote source, or manually curated).
enforcement — Default enforcement mode:
"deny"— Hard deny unsigned/invalid/untrusted files (production default)"warn"— Log warning but allow access (migration/adoption mode)"audit"— Allow access, log verification result for post-hoc review
override_flag — Documents the CLI flag that bypasses enforcement for development. This is informational; the actual override is a CLI argument, not a policy setting.
Trust policy files are composable. Multiple trust-policy.json files can exist at different levels:
- Embedded default — Ships with nono CLI (built into the binary via
build.rs), defines baseline instruction patterns and an empty publisher list. - User-level —
$XDG_CONFIG_HOME/nono/trust-policy.json, defines the user's trusted publishers and personal key IDs. - Project-level —
<project-root>/trust-policy.jsonor<project-root>/.nono/trust-policy.json, defines project-specific publishers.
Resolution order: embedded defaults are loaded first, then user-level, then project-level. Publishers are merged (union). Blocklist digests are merged (union). Instruction patterns are merged (union). Enforcement uses the strictest level (deny > warn > audit).
Project-level trust policy cannot weaken user-level or embedded policy. A project cannot remove a blocklist entry or downgrade enforcement from deny to warn. This prevents a malicious project from disabling verification via its own trust policy.
The keyless signing mode combined with trust policy publishers creates a "trusted publishers" framework analogous to PyPI's trusted publishers.
name: Sign instruction files
on:
push:
branches: [main]
paths:
- 'SKILLS.md'
- 'CLAUDE.md'
- '.claude/**/*.md'
permissions:
id-token: write
contents: write
jobs:
sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install nono
run: cargo install nono-cli
- name: Sign instruction files
run: |
for f in SKILLS.md CLAUDE.md; do
[ -f "$f" ] && nono trust sign "$f" --keyless
done
- name: Commit bundles
run: |
git add *.bundle
git commit -m "Update instruction file signatures" || true
git pushThe GitHub Actions OIDC token provides ambient credentials. Nono's --keyless flag:
- Detects the GitHub Actions environment via
ACTIONS_ID_TOKEN_REQUEST_URL - Requests an OIDC token with the Sigstore audience
- Exchanges the token with Fulcio for a short-lived certificate
- Signs the DSSE envelope with the ephemeral key
- Submits the signature to Rekor for transparency logging
- Produces the
.bundlefile
When nono verifies a bundle produced by keyless signing, it extracts the OIDC identity from the Fulcio certificate extensions:
Certificate extensions:
1.3.6.1.4.1.57264.1.1 (issuer): https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.8 (repository): anthropics/claude-skills
1.3.6.1.4.1.57264.1.9 (ref): refs/tags/v1.2.3
1.3.6.1.4.1.57264.1.11 (workflow): .github/workflows/sign-skills.yml
These are matched against the publishers entries in the trust policy. The verification passes only if:
- The Sigstore bundle is cryptographically valid (signature, Fulcio cert chain, Rekor inclusion proof)
- The Fulcio certificate was valid at signing time (verified via Rekor timestamp)
- The file's SHA-256 digest matches the
subject.digestin the DSSE statement - The certificate's OIDC claims match at least one
publishersentry in the trust policy
This means an organization can declare: "Only trust SKILLS.md files signed by a GitHub Actions workflow in the anthropics/claude-skills repository, from a tagged release." A fork, manual edit, or compromised developer workstation cannot produce a matching signature.
Verification is enforced at multiple points depending on platform and execution strategy.
Before fork/exec, nono scans the working directory (and any explicitly granted paths) for files matching instruction_patterns. For each match:
- Check blocklist — if digest matches a known-bad entry, abort immediately
- Locate the
.bundlefile — if missing, enforcement determines action (deny/warn/audit) - Verify the bundle — cryptographic validation + trust policy publisher match
- On failure — enforcement determines action
nono run --profile claude-code -- claude
-> Scan CWD for instruction file patterns
-> For each match:
-> Compute SHA-256 digest
-> Check blocklist
-> Load and verify .bundle
-> Check publisher identity against trust policy
-> All pass: proceed with sandbox application and exec
-> Any fail (enforcement=deny): abort with diagnostic message
Pre-exec verification is the baseline. It catches instruction files that are present at session start — the most common case, since agent frameworks typically read instruction files at initialization.
On Linux in Supervised mode, the seccomp-notify supervisor already traps openat syscalls. When the supervisor reads the target path from /proc/PID/mem and the path matches an instruction file pattern:
- Supervisor reads the file content from disk (it has unrestricted access)
- Computes SHA-256 digest, checks blocklist
- Locates and verifies the
.bundlefile - Valid: injects fd via
SECCOMP_IOCTL_NOTIF_ADDFD - Invalid/missing: returns EPERM
This catches instruction files discovered at runtime — for example, a SKILLS.md in a subdirectory that the agent navigates to during the session, or an instruction file unpacked from a dependency.
The verification result is cached by (path, inode, mtime) tuple to avoid re-verifying the same file on repeated opens.
On macOS in Supervised mode, when the agent requests dynamic capability expansion for a path matching an instruction file pattern, the supervisor performs verification before presenting the approval prompt. If the file fails verification, the request is denied without prompting the user — similar to never_grant behavior.
To avoid re-computing signatures on every access:
Cache key: (canonical_path, inode, mtime, file_size)
Cache value: (sha256_digest, verification_result, publisher_name)
TTL: Session lifetime (cleared on nono exit)
If any component of the cache key changes (file modified, replaced, etc.), the cache entry is invalidated and verification runs again.
Sign an instruction file, producing a .bundle alongside it.
nono trust sign SKILLS.md # keyed (default key from keystore)
nono trust sign SKILLS.md --key <key-id> # keyed (specific key)
nono trust sign SKILLS.md --keyless # keyless (OIDC + Fulcio + Rekor)
nono trust sign SKILLS.md CLAUDE.md # multiple files
nono trust sign --all # all files matching instruction_patterns in CWD
Verify an instruction file's bundle against the trust policy.
nono trust verify SKILLS.md # verify single file
nono trust verify --all # verify all instruction files in CWD
nono trust verify SKILLS.md --policy <path> # use specific trust policy
Output on success:
SKILLS.md: VERIFIED
Signer: anthropic-official (keyless)
Repository: anthropics/claude-skills
Workflow: .github/workflows/sign-skills.yml
Ref: refs/tags/v1.2.3
Signed: 2026-02-20T10:00:00Z
Digest: sha256:a1b2c3d4e5f6...
Output on failure:
SKILLS.md: FAILED
Reason: No matching publisher in trust policy
Bundle signer: repo:unknown-org/unknown-repo
Expected publishers: anthropic-official, my-org, local-dev
List all instruction files in CWD and their verification status.
nono trust list
File Status Publisher
SKILLS.md VERIFIED anthropic-official
CLAUDE.md VERIFIED local-dev (keyed)
.claude/commands/deploy.md UNSIGNED -
.claude/commands/test.md FAILED bundle digest mismatch
Generate a new ECDSA P-256 key pair and store the private key in the system keystore.
nono trust keygen # default key ID
nono trust keygen --id my-signing-key # named key
The library provides attestation primitives reusable by all language bindings.
crates/nono/src/trust/
├── mod.rs # Public API re-exports
├── types.rs # TrustPolicy, Publisher, BlocklistEntry, VerificationResult
├── digest.rs # SHA-256 digest computation for files
├── dsse.rs # DSSE envelope parsing and PAE construction
├── bundle.rs # Sigstore bundle verification (wraps sigstore-rs crates)
├── policy.rs # Trust policy evaluation (publisher matching, blocklist check)
└── cache.rs # Verification result cache (path, inode, mtime keyed)
New dependencies:
sigstore-verify— Bundle verification, Fulcio cert chain, Rekor proofsigstore-bundle— Bundle parsing and structural validationsigstore-types— Core Sigstore data structuressigstore-crypto— ECDSA P-256 signature verificationsigstore-trust-root— Sigstore trusted root (TUF-based)
For signing (library, optional feature flag signing):
sigstore-sign— Signing workflow orchestrationsigstore-fulcio— Certificate issuancesigstore-rekor— Transparency log submissionsigstore-oidc— OIDC token acquisition
crates/nono-cli/src/
├── trust_cmd.rs # nono trust sign/verify/list/keygen subcommands
├── trust_scan.rs # Pre-exec instruction file scanning
└── trust_intercept.rs # Integration with supervisor message handling
Data files:
crates/nono-cli/data/
├── policy.json # Sandbox policy (existing)
└── trust-policy.json # Default trust policy (instruction patterns, empty publishers)
| In Library | In CLI |
|---|---|
| DSSE parsing, PAE verification | nono trust subcommands |
| Sigstore bundle verification | Trust policy file loading and merging |
| Trust policy data types and evaluation | Pre-exec scanning |
| Digest computation | Supervisor integration (seccomp-notify, macOS gating) |
| Verification cache | Instruction file pattern matching from config |
| Signing primitives (behind feature flag) | Override flag (--trust-override) |
┌─────────────────────┐
│ File access detected │
│ (pre-exec or runtime)│
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Matches instruction │──── No ───→ Allow (not an instruction file)
│ file pattern? │
└──────────┬──────────┘
│ Yes
┌──────────▼──────────┐
│ Compute SHA-256 │
│ digest of file │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Check blocklist │──── Match ──→ HARD DENY (known malicious)
└──────────┬──────────┘
│ No match
┌──────────▼──────────┐
│ Locate .bundle file │──── Missing ─→ Enforcement mode
└──────────┬──────────┘ (deny/warn/audit)
│ Found
┌──────────▼──────────┐
│ Verify Sigstore │──── Invalid ─→ HARD DENY
│ bundle (crypto) │ (signature/cert/Rekor)
└──────────┬──────────┘
│ Valid
┌──────────▼──────────┐
│ Extract signer │
│ identity from cert │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Match against trust │──── No match ─→ HARD DENY
│ policy publishers │ (untrusted publisher)
└──────────┬──────────┘
│ Match
┌──────────▼──────────┐
│ Digest in statement │──── Mismatch ─→ HARD DENY
│ matches file? │ (file tampered)
└──────────┬──────────┘
│ Match
┌──────────▼──────────┐
│ Cache result │
│ ALLOW │
└─────────────────────┘
For development and testing, a --trust-override flag disables attestation enforcement:
nono run --profile claude-code --trust-override -- claude
When active:
- Verification still runs and results are logged
- Failures produce warnings to stderr instead of hard denies
- The diagnostic banner includes a warning that trust verification is overridden
This flag is a CLI argument only. It cannot be set in a profile or trust policy (a project cannot disable its own verification). Environment variable NONO_TRUST_OVERRIDE=1 is also supported for CI/CD convenience but logs a prominent warning.
Bundle must be verified before the agent reads the file. The pre-exec scan and seccomp-notify interception both ensure the agent never sees content that failed verification. This is critical — if the agent reads the file before verification completes, the injection has already succeeded.
Trust policy cannot be weakened by project-level config. A malicious project's trust-policy.json can add publishers but cannot remove blocklist entries, remove instruction patterns, or downgrade enforcement. Merging is additive for restrictions, not subtractive.
Blocklist is checked before bundle verification. A known-malicious file is denied even if it has a valid signature. This handles the case of a legitimately-signed file that is later discovered to be malicious.
Cache invalidation on file modification. The cache keys on (path, inode, mtime, size). If any of these change, the cached result is discarded and verification re-runs. This prevents TOCTOU attacks where a file is modified after initial verification.
Private key protection. For keyed signing, the private key lives in the system keystore (macOS Keychain, Linux Secret Service). It is never written to disk in plaintext. The keystore module already handles this.
Fulcio certificate short-livedness. Keyless signatures use short-lived certificates (typically 10-20 minutes). The Rekor timestamp proves the signature was created while the certificate was valid. Verification checks this timestamp against the Rekor inclusion proof, not against the current time.
No trust-on-first-use (TOFU). Unlike the earlier SKILLS-Research.md proposal (Extension 3), this design does not use TOFU. A file must have a valid signature from a trusted publisher on first encounter. TOFU creates a window where the first encounter is untrusted, which is exactly the supply chain attack scenario we're defending against.
- sigstore-rust — Sigstore Rust SDK (merging into sigstore org)
- DSSE specification — Dead Simple Signing Envelope
- in-toto attestation — Attestation framework
- SLSA provenance — Supply chain provenance specification
- Sigstore bundle spec — Bundle format specification
- Fulcio OIDC extensions — Certificate extension OIDs