Skip to content

registry for profiles and agent packs #459

Description

@lukehinds

Add a package distribution system that lets agent-specific artifacts (profiles, hooks, instruction files, trust policies, policy groups, scripts) live out-of-tree as cryptographically signed packages, installable via nono pull or setup directly from a repository. Publishing happens through GitHub Actions with Sigstore keyless signing. The client verifies provenance locally - the registry or any intermediary is a distribution channel, not a trust anchor, or mandatory part of the workflow.

Summary

Add a package distribution system that lets agent-specific artifacts (profiles, hooks, instruction files, trust policies, policy groups, scripts) live out-of-tree as cryptographically signed packages, installable via nono pull or setup directly from a repository. Publishing happens through GitHub Actions with Sigstore keyless signing. The client verifies provenance locally - the registry or any intermediary is a distribution channel, not a trust anchor, or mandatory part of the workflow.

Motivation

Agent-specific artifacts are currently hardcoded into the nono binary or require manual setup. Adding support for a new coding agent means modifying policy.json, embedding hook scripts in build.rs, and adding agent-detection logic. This creates maintenance burden (#404), muddy separation of concerns (#353), and inflexible hook/script management (#407).

The package system moves this knowledge out of core. A package author declares what files their agent needs, where they go, and how they're verified. Core maintainers maintain the sandbox primitive and the package infrastructure - not per-agent configuration for every tool on the market.

Trust model

The registry is not a trust anchor. Every artifact carries a Sigstore bundle containing a Fulcio certificate with embedded OIDC claims (issuer, repository, workflow, ref) and a Rekor transparency log inclusion proof. The client verifies these bundles locally using the Sigstore trust root.

Even if the registry were fully compromised, an attacker cannot forge valid artifacts for a namespace they don't control. Producing a valid bundle for acme-corp/claude-policy requires a GitHub Actions OIDC token issued to an acme-corp/* repository.

Publishing flow

1. Author pushes to GitHub (e.g., acme-corp/nono-policies)
2. GitHub Action triggers:
   a. Signs all artifacts with Sigstore (keyless OIDC via Fulcio + Rekor)
   b. Uploads artifacts + bundles to registry via API
3. Registry receives upload:
   a. Verifies Sigstore bundles (Fulcio cert chain, Rekor inclusion proof)
   b. Asserts OIDC claims match the registered trusted publisher
   c. Runs security scans (prompt injection detection, policy analysis)
   d. Stores artifacts + bundles + scan metadata
4. User: nono pull acme-corp/claude-policy
   a. Downloads artifacts + bundles from registry
   b. Verifies bundles locally (Sigstore trust root, not registry trust)
   c. Asserts signer org matches the namespace in the pull command
   d. Pins signer identity in lockfile
   e. Installs verified artifacts

Verification on pull

Check What it proves
Sigstore bundle verification Artifacts signed by a valid Fulcio certificate, logged in Rekor
Signer identity extraction Fulcio certificate identifies the signing workflow and repository
Namespace assertion Signer's repository org matches the requested namespace
Signer consistency All artifacts in the package share the same signer identity
Digest verification Local SHA-256 matches the registry-reported digest
Signer pinning On update, signer identity matches the previously recorded identity

Package manifest (package.json)

Every package contains a package.json manifest that declares its artifacts and installation behavior:

{
  "schema_version": 1,
  "name": "claude-code",
  "description": "Sandbox profile, hooks, and trust policy for Claude Code",
  "license": "Apache-2.0",
  "platforms": ["macos", "linux"],
  "min_nono_version": "0.19.0",
  "artifacts": [
    {
      "type": "profile",
      "path": "claude-code.profile.json",
      "install_as": "claude-code"
    },
    {
      "type": "hook",
      "path": "hooks/nono-hook.sh",
      "target": "claude-code",
      "install_dir": "~/.claude/hooks"
    },
    {
      "type": "instruction",
      "path": "CLAUDE.md",
      "placement": "project"
    },
    {
      "type": "groups",
      "path": "groups.json",
      "prefix": "claude_code"
    },
    {
      "type": "trust_policy",
      "path": "trust-policy.json",
      "merge_strategy": "additive"
    }
  ]
}

Artifact types

Type Package store path External placement Behavior
profile <pkg>/profiles/<install_as>.json Symlinked into ~/.config/nono/profiles/ Validated as a nono profile. Symlink makes it discoverable by the existing profile loader.
hook <pkg>/hooks/<filename> install_dir (e.g. ~/.claude/hooks) Script file. Not executed during install. Registered in target app settings.
instruction <pkg>/instructions/<filename> CWD (with --init) CLAUDE.md, SKILLS.md, etc.
trust_policy <pkg>/trust-policy.json - Merged additively at load time. Cannot weaken existing policy.
groups <pkg>/groups.json - Additional policy groups. All names must start with declared prefix. Loaded alongside the embedded policy.
script <pkg>/scripts/<filename> install_dir (if declared) Utility scripts. Made executable. Not auto-executed.

Manifest-driven install paths

Artifacts with install_dir are placed at the declared path (with ~ expansion) in addition to the package store copy. This lets package authors target agent-specific locations (~/.cursor/rules/, ~/.claude/hooks/, etc.) without requiring nono core to know about every agent's conventions.

External placements are tracked in the lockfile so nono remove can clean them up.

Installation flows

There are two ways to install package artifacts. Both use the same signed artifact format and the same Sigstore verification, but differ in how artifacts are retrieved and placed.

Flow 1: Registry pull (nono pull)

The managed path. The CLI handles download, verification, installation, lockfile tracking, and cleanup.

nono pull nono-project/claude-code
nono pull nono-project/claude-code@1.2.0
nono pull nono-project/claude-code --init

Flags:

  • --force - overwrite conflicts, accept signer identity changes
  • --init - copy instruction files with placement: "project" into the current directory

Pull flow:

  1. Parse and validate package reference
  2. Fetch pull manifest from registry
  3. Check lockfile - skip if already up to date
  4. Download all artifacts and their Sigstore bundles
  5. Verify each bundle locally, assert namespace, check signer consistency, verify digests
  6. Enforce signer pinning against lockfile
  7. Parse and validate package.json manifest (platform check, min version)
  8. Stage to a temp directory
  9. Install artifacts by type, honoring install_dir where declared
  10. Atomic rename from staging to final path
  11. Create profile symlinks
  12. Update lockfile with version, provenance, artifact digests, and external paths
  13. Print provenance summary

Flow 2: Direct repository setup

The self-managed path. The package author keeps signed artifacts (with .bundle sidecars) in a Git repository and provides their own setup instructions or script. Consumers clone the repo, verify signatures, and place files where they need to go.

Example workflow from the consumer side:

git clone https://github.com/acme-corp/nono-policies
cd nono-policies/packages/claude-code

# Verify every artifact against its Sigstore bundle or have nono verify at runtime / sandbox creation
nono trust verify claude-code.profile.json
nono trust verify hooks/nono-hook.sh
nono trust verify CLAUDE.md

# Place files manually (or via a setup script the author provides)
cp claude-code.profile.json ~/.config/nono/profiles/claude-code.json
cp hooks/nono-hook.sh ~/.claude/hooks/nono-hook.sh
cp CLAUDE.md ./CLAUDE.md

The package author could also ship a setup script alongside the artifacts to automate placement. nono does not run or manage this script - it is entirely out-of-band.

What this path provides:

  • Full cryptographic provenance via Sigstore bundles
  • Identical sandbox enforcement once files are in place

What this path does not provide:

  • No nono pull / nono remove lifecycle management
  • No signer pinning or lockfile tracking

Both flows produce the same end state: verified artifacts on disk that the sandbox enforces identically.

CLI commands

nono pull <namespace>/<name>[@<version>]

Install a signed package from the registry. See "Flow 1: Registry pull" above for the full flow.

nono remove <namespace>/<name>

Remove an installed package and clean up all artifacts:

  • Remove externally placed files tracked in the lockfile
  • Unregister hooks from target app settings (e.g. remove entries from ~/.claude/settings.json)
  • Remove hook script files from target directories
  • Remove all profile symlinks pointing into the package directory
  • Remove the package store directory
  • Remove lockfile entry

nono search <query>

Search the registry for packages.

nono search claude
nono search sandbox --json

nono list --installed

List installed packages from the lockfile.

nono list --installed
nono list --installed --json

nono update [<namespace>/<name>]

Update installed packages to newer versions from the registry. Not yet implemented.

Lockfile

The lockfile (~/.config/nono/packages/lockfile.json) tracks installed packages, their provenance, artifact digests, and external file placements:

{
  "lockfile_version": 1,
  "registry": "https://registry.nono.sh",
  "packages": {
    "nono-project/claude-code": {
      "version": "1.2.0",
      "installed_at": "2026-03-17T10:00:00Z",
      "provenance": {
        "signer_identity": "https://github.com/nono-project/packages/.github/workflows/publish.yml@refs/heads/main",
        "repository": "nono-project/packages",
        "workflow": ".github/workflows/publish.yml",
        "ref": "refs/heads/main",
        "rekor_log_index": 12345678,
        "signed_at": "2026-03-17T10:00:00Z"
      },
      "artifacts": {
        "claude-code.profile.json": {
          "sha256": "abc123...",
          "type": "profile"
        },
        "hooks/nono-hook.sh": {
          "sha256": "def456...",
          "type": "hook",
          "installed_path": "/Users/dev/.claude/hooks/nono-hook.sh"
        }
      }
    }
  }
}

Signer pinning

On subsequent pulls, the client compares the new signer identity against the lockfile. If it changes - even if cryptographically valid - the update is rejected unless --force is used. This prevents registry-side substitution of artifacts signed by a different identity.

Hook script externalization

When a profile comes from a package, hook scripts are resolved through a fallback chain:

  1. Package store (if the active profile came from a package)
  2. User override (~/.config/nono/hooks/<script_name>)
  3. Embedded script (backward compat for built-in profiles)

This means package authors control their own hook scripts without requiring changes to the nono binary.

Policy group loading

Packages can ship additional policy groups via a groups.json artifact. When the policy system loads, it reads installed packages from the lockfile, loads each package's groups, validates that group names start with the declared prefix, rejects collisions with built-in groups, and merges them into the effective policy.

This lets packages define agent-specific filesystem rules (e.g. Claude Code's macOS keychain access, Linux state paths) without modifying the embedded policy.

Local package store layout

~/.config/nono/
  profiles/
    claude-code.json -> ../packages/nono-project/claude-code/profiles/claude-code.json
  packages/
    lockfile.json
    nono-project/
      claude-code/
        package.json
        profiles/claude-code.profile.json
        hooks/nono-hook.sh
        instructions/CLAUDE.md
        groups.json

Known limitations

  • install_dir supports ~ expansion and absolute paths, but not environment variable expansion ($HOME, $XDG_CONFIG_HOME). Package authors targeting both Linux and macOS need to use ~-based paths that work on both platforms, or ship separate platform-specific artifacts. Most agents use dotfiles under $HOME so this is not a practical issue today, but it means agents with platform-divergent config locations (e.g. ~/Library/Application Support/ on macOS vs ~/.config/ on Linux for the same file) would need two artifacts.
  • nono update is not yet implemented.
  • Trust policy merge at load time is not yet wired (the artifact type is supported, but the merge logic is not connected).
  • No nono push - publishing is GitHub Actions only, by design.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions