Skip to content

Instantly share code, notes, and snippets.

@lukehinds
Created February 20, 2026 12:44
Show Gist options
  • Select an option

  • Save lukehinds/1965883185629d5ecce316fdf1bab758 to your computer and use it in GitHub Desktop.

Select an option

Save lukehinds/1965883185629d5ecce316fdf1bab758 to your computer and use it in GitHub Desktop.

Design: Instruction File Attestation and Integrity Verification

Status: Proposed Date: 2026-02-20


Problem

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.

Threat Model

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

Goal

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.


Solution: Sigstore-Based Attestation

Signing Modes

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.

Attestation Format: DSSE + In-Toto Statement

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.

Signature Storage Convention

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

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.

File: trust-policy.json

{
  "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"
}

Policy Fields

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.

Policy Composition

Trust policy files are composable. Multiple trust-policy.json files can exist at different levels:

  1. Embedded default — Ships with nono CLI (built into the binary via build.rs), defines baseline instruction patterns and an empty publisher list.
  2. User-level$XDG_CONFIG_HOME/nono/trust-policy.json, defines the user's trusted publishers and personal key IDs.
  3. Project-level<project-root>/trust-policy.json or <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.


Trusted Publishers via GitHub Actions

The keyless signing mode combined with trust policy publishers creates a "trusted publishers" framework analogous to PyPI's trusted publishers.

Signing in GitHub Actions

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 push

The GitHub Actions OIDC token provides ambient credentials. Nono's --keyless flag:

  1. Detects the GitHub Actions environment via ACTIONS_ID_TOKEN_REQUEST_URL
  2. Requests an OIDC token with the Sigstore audience
  3. Exchanges the token with Fulcio for a short-lived certificate
  4. Signs the DSSE envelope with the ephemeral key
  5. Submits the signature to Rekor for transparency logging
  6. Produces the .bundle file

Verification of Publisher Identity

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:

  1. The Sigstore bundle is cryptographically valid (signature, Fulcio cert chain, Rekor inclusion proof)
  2. The Fulcio certificate was valid at signing time (verified via Rekor timestamp)
  3. The file's SHA-256 digest matches the subject.digest in the DSSE statement
  4. The certificate's OIDC claims match at least one publishers entry 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.


Interception Points

Verification is enforced at multiple points depending on platform and execution strategy.

Pre-exec Verification (Both Platforms)

Before fork/exec, nono scans the working directory (and any explicitly granted paths) for files matching instruction_patterns. For each match:

  1. Check blocklist — if digest matches a known-bad entry, abort immediately
  2. Locate the .bundle file — if missing, enforcement determines action (deny/warn/audit)
  3. Verify the bundle — cryptographic validation + trust policy publisher match
  4. 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.

Seccomp-Notify Interception (Linux Runtime)

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:

  1. Supervisor reads the file content from disk (it has unrestricted access)
  2. Computes SHA-256 digest, checks blocklist
  3. Locates and verifies the .bundle file
  4. Valid: injects fd via SECCOMP_IOCTL_NOTIF_ADDFD
  5. 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.

Supervisor Gating (macOS Supervised Mode)

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.

Verification Cache

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.


CLI Commands

nono trust sign <file> [options]

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

nono trust verify <file> [options]

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

nono trust list

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

nono trust keygen

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

Codebase Location

Library (crates/nono/src/trust/)

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 proof
  • sigstore-bundle — Bundle parsing and structural validation
  • sigstore-types — Core Sigstore data structures
  • sigstore-crypto — ECDSA P-256 signature verification
  • sigstore-trust-root — Sigstore trusted root (TUF-based)

For signing (library, optional feature flag signing):

  • sigstore-sign — Signing workflow orchestration
  • sigstore-fulcio — Certificate issuance
  • sigstore-rekor — Transparency log submission
  • sigstore-oidc — OIDC token acquisition

CLI (crates/nono-cli/)

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)

Library vs CLI Boundary

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)

Verification Flow

                    ┌─────────────────────┐
                    │ 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               │
                    └─────────────────────┘

Development Override

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.


Security Considerations

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.


References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment