Skip to content
Provenance

Cryptographic Provenance for Coding-Agent Output

Use Sigstore keyless signing to bind agent identity, model version, and policy context into the OIDC token of every commit and artifact an autonomous coding agent produces.

Advanced 11 min read Updated May 2026

When a human engineer signs a commit, the cryptographic identity binds back to a person — a verified email, a hardware key, a Yubikey tap. When a coding agent signs a commit, what identity should the certificate bind to?

This is the question that keyless signing of agent output forces you to answer. Generic Sigstore signing (covered in our sigstore-signing article) treats the build runner as the signer. That works when the build runner is trustworthy and the question is "did our pipeline build this?" It does not answer the question that matters when 35% of internal merged PRs at Cursor are agent-authored (per CEO Michael Truell's disclosure, late February / early March 2026): which agent, with which model version, under which policy, produced this exact commit?

This article covers the agent-identity layer of Sigstore: how to encode model version, agent name, prompt-hash, and policy-pack-hash into the OIDC token Fulcio sees, so the resulting Rekor entry is a self-describing record of agent action, not just a build provenance receipt.

The Identity Gap in Agent Signing

A standard GitHub Actions Sigstore signature looks like this in Rekor:

{
  "kind": "hashedrekord",
  "spec": {
    "signature": {
      "publicKey": {
        "content": "..."
      }
    }
  },
  "verification": {
    "certificate": {
      "subjectAlternativeName": "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main",
      "issuer": "https://token.actions.githubusercontent.com"
    }
  }
}

The certificate identity says: "GitHub Actions ran build.yml on main in myorg/myrepo." That is enough to defeat artifact-substitution attacks. It is not enough to answer:

  • Was a Copilot Coding Agent the upstream commit author?
  • Which model version did it use?
  • What was the prompt-hash and the policy-pack-hash that were active?
  • Did a human review the agent output before the build ran?

If your incident response runs cosign verify and gets back nothing more than "GitHub Actions built this from commit abc123," you have artifact provenance but no agent provenance. To close the gap, the agent itself — or the orchestrator that ran it — must be the signing identity, and the OIDC token must carry the agent metadata as claims.

OIDC Token Claims for Agent Identity

Sigstore's Fulcio CA encodes the entire OIDC token's relevant claims into the X.509 certificate via custom OIDs. That means any claim you put in the token at signing time becomes an immutable, publicly-auditable part of the Rekor entry forever.

For agent provenance, the claim shape that matters:

{
  "iss": "https://agents.crashoverride.com",
  "sub": "agent:copilot-coding-agent-v3",
  "aud": "sigstore",
  "exp": 1746115935,
  "iat": 1746115335,
  "agent_name": "copilot-coding-agent",
  "agent_version": "v3.2.1",
  "model_id": "gpt-4-turbo-2024-12",
  "model_provider": "openai",
  "policy_pack_hash": "sha256:9d4c7e2b...",
  "prompt_hash": "sha256:e1a8b3d4...",
  "human_reviewer": "[email protected]",
  "review_timestamp": "2026-05-01T14:32:15Z",
  "repo": "crashoverride/api-gateway",
  "commit_sha": "def456ghi789"
}

You issue this token from your agent orchestrator (an internal OIDC IdP that knows which agent ran), exchange it for a Fulcio cert, sign the artifact, and the Rekor entry now contains a verifiable record of agent identity, model, policy, and human review.

End-to-End Workflow

Step 1: Stand Up an Agent OIDC Issuer

Sigstore Fulcio accepts any OIDC issuer that's been added to its trust root, but for internal use you typically run your own issuer and configure cosign to accept it via --fulcio-url and --rekor-url pointing at a private Sigstore deployment, or use the public good instance with a federated identity.

A minimal agent-issuer JWT signing service exposes:

  • /.well-known/openid-configuration
  • /.well-known/jwks.json
  • A token endpoint your agent orchestrator calls when it's about to sign

The token endpoint takes the agent's runtime context — model version, policy pack hash, prompt hash, human reviewer — and bakes it into the JWT claims.

Step 2: Hash the Inputs at Run Time

Before the agent starts, compute and stash hashes:

# Hash the prompt the agent received
PROMPT_HASH=$(printf '%s' "$AGENT_PROMPT" | sha256sum | awk '{print $1}')

# Hash the policy pack (the rules file the agent must follow)
POLICY_HASH=$(sha256sum policies/agent-policy.yaml | awk '{print $1}')

# Capture model version from the API response, not from config
MODEL_ID=$(curl -s https://api.openai.com/v1/models/gpt-4-turbo \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  | jq -r .id)

export PROMPT_HASH POLICY_HASH MODEL_ID

Pinning MODEL_ID from the API response (not from a config file) is the trick that defends against silent model upgrades — if OpenAI rolls gpt-4-turbo forward, the hash changes, and your provenance reflects the actual model.

Step 3: Mint the Agent OIDC Token

TOKEN=$(curl -sX POST https://agents.crashoverride.com/oauth/token \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg agent "copilot-coding-agent" \
    --arg version "v3.2.1" \
    --arg model "$MODEL_ID" \
    --arg prompt "$PROMPT_HASH" \
    --arg policy "$POLICY_HASH" \
    --arg reviewer "[email protected]" \
    '{agent_name:$agent, agent_version:$version, model_id:$model, prompt_hash:$prompt, policy_pack_hash:$policy, human_reviewer:$reviewer}')")

Step 4: Sign the Artifact with Agent Identity

cosign sign \
  --identity-token "$TOKEN" \
  --fulcio-url https://fulcio.crashoverride.com \
  --rekor-url https://rekor.crashoverride.com \
  ghcr.io/crashoverride/api-gateway@sha256:def456...

The certificate Fulcio issues now carries the agent claims. Anyone who later runs cosign verify and inspects the certificate gets the full agent context.

Step 5: Verify with Identity Constraints

The verification step is where this pays off. Instead of accepting any signature, you constrain the agent identity:

cosign verify \
  --certificate-identity-regexp '^agent:copilot-coding-agent$' \
  --certificate-oidc-issuer https://agents.crashoverride.com \
  --certificate-github-workflow-trigger 'pull_request' \
  ghcr.io/crashoverride/api-gateway@sha256:def456...

To pull the agent metadata back out:

cosign verify \
  --certificate-identity-regexp '^agent:.*' \
  --certificate-oidc-issuer https://agents.crashoverride.com \
  --output json \
  ghcr.io/crashoverride/api-gateway@sha256:def456... \
  | jq '.[].optional.Bundle.Payload.body | @base64d | fromjson | .spec.signature.publicKey.content'

For a richer view of all agent claims, query Rekor directly:

LOG_INDEX=$(rekor-cli search --sha sha256:def456... | tail -n1)
rekor-cli get --log-index "$LOG_INDEX" --format json \
  | jq '.Body.HashedRekordObj.signature.publicKey'

Policy Enforcement: Reject Unverified Agents

Once agent identity is in the certificate, your admission controllers can refuse anything that doesn't meet your agent policy. A Kubernetes ClusterImagePolicy:

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: agent-authored-images
spec:
  images:
    - glob: "ghcr.io/crashoverride/*"
  authorities:
    - name: approved-agents
      keyless:
        url: https://fulcio.crashoverride.com
        identities:
          - issuer: https://agents.crashoverride.com
            subjectRegExp: '^agent:(copilot-coding-agent|cursor-agent|claude-code)$'
      attestations:
        - name: must-have-human-review
          predicateType: https://crashoverride.com/agent-review/v1
          policy:
            type: cue
            data: |
              predicate: human_reviewer: != ""
              predicate: review_timestamp: != ""

The policy says: only accept artifacts where the signing identity matches an approved agent name AND the attestation proves a human reviewer signed off. An agent operating without review fails admission.

Agent Attestations Beyond the Signature

The certificate is one signal. The richer payload is in attestations. For each agent run, generate an in-toto attestation that captures the full decision context:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{
    "name": "ghcr.io/crashoverride/api-gateway",
    "digest": { "sha256": "def456ghi789..." }
  }],
  "predicateType": "https://crashoverride.com/agent-run/v1",
  "predicate": {
    "agent": {
      "name": "copilot-coding-agent",
      "version": "v3.2.1"
    },
    "model": {
      "id": "gpt-4-turbo-2024-12",
      "provider": "openai",
      "temperature": 0.2,
      "max_tokens": 4096
    },
    "input": {
      "prompt_hash": "sha256:e1a8b3d4...",
      "context_files": ["src/api/health.ts", "src/api/auth.ts"],
      "context_token_count": 6244
    },
    "policy": {
      "pack_hash": "sha256:9d4c7e2b...",
      "rules_evaluated": 47,
      "rules_violated": 0
    },
    "tools_invoked": [
      { "name": "edit_file", "count": 3 },
      { "name": "run_tests", "count": 1, "exit_code": 0 }
    ],
    "human_review": {
      "reviewer": "[email protected]",
      "reviewed_at": "2026-05-01T14:32:15Z",
      "review_duration_seconds": 187,
      "approved": true
    },
    "git": {
      "commit_sha": "def456ghi789",
      "files_changed": ["src/api/health.ts"],
      "lines_added": 24,
      "lines_removed": 3
    }
  }
}

Attach it to the artifact:

cosign attest \
  --predicate agent-run.json \
  --type "https://crashoverride.com/agent-run/v1" \
  --identity-token "$TOKEN" \
  ghcr.io/crashoverride/api-gateway@sha256:def456...

Now cosign verify-attestation returns the full run context. During an incident, you can query: "Show me every artifact in production where the agent's review duration was less than 60 seconds" — and find the ones where humans rubber-stamped agent output.

Why This Differs from Generic Artifact Signing

Generic Sigstore signing (the sigstore-signing article) answers: did our build system produce this artifact, and was it tampered with? The signing identity is the GitHub Actions runner.

Agent-output cryptographic provenance answers: which agent, on which model, under which policy, produced this — and did a human approve? The signing identity is the agent itself, with model and policy hashes baked into the OIDC claims and Rekor entry.

Both layers compose. A robust pipeline signs twice: once with the agent's identity at the moment of code generation, and again with the build runner's identity when the artifact is produced. The artifact carries two certificate chains and two attestations. Verifiers can require both.

Operational Realities

  • Rekor is public. Anything in the public good Rekor instance is searchable forever. If your prompt_hash reveals confidential prompt content via reverse-lookup, run a private Rekor.
  • Token lifetime is 10 minutes. Mint the JWT immediately before signing. Long-lived agent runs need to re-mint per artifact.
  • Model version drift is the real enemy. Pin model_id from the API response, not config — providers roll model pointers without warning.
  • Human-review claims must be verifiable independently. A claim of human_reviewer: alice@... is only as strong as the system that issued the token. Tie it to a GitHub PR review event or an admin-portal approval workflow that itself logs to an immutable store.

What to Build Next

Once cryptographic agent provenance is wired in, the next layer is SLSA Build Track 3 hermeticity for the agent-driven build itself — covered in our companion piece on SLSA Build Track Level 3 for Agent-Generated Artifacts. Together they form the two halves of a defensible agent supply chain: who wrote it (this article) and how it was built (the next).

Sources

This article is part of the Provenance knowledge series (4 articles) Browse all Provenance articles →
Related Use Case

AI Code Traceability — Your developers don't write the code

Nobody has control anymore. Leaders have visibility.

Explore Use Case →