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:
- Parse and validate package reference
- Fetch pull manifest from registry
- Check lockfile - skip if already up to date
- Download all artifacts and their Sigstore bundles
- Verify each bundle locally, assert namespace, check signer consistency, verify digests
- Enforce signer pinning against lockfile
- Parse and validate
package.json manifest (platform check, min version)
- Stage to a temp directory
- Install artifacts by type, honoring
install_dir where declared
- Atomic rename from staging to final path
- Create profile symlinks
- Update lockfile with version, provenance, artifact digests, and external paths
- 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:
- Package store (if the active profile came from a package)
- User override (
~/.config/nono/hooks/<script_name>)
- 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.
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 pullor 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 pullor 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 inbuild.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-policyrequires a GitHub Actions OIDC token issued to anacme-corp/*repository.Publishing flow
Verification on pull
Package manifest (
package.json)Every package contains a
package.jsonmanifest 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
profile<pkg>/profiles/<install_as>.json~/.config/nono/profiles/hook<pkg>/hooks/<filename>install_dir(e.g.~/.claude/hooks)instruction<pkg>/instructions/<filename>--init)trust_policy<pkg>/trust-policy.jsongroups<pkg>/groups.jsonprefix. Loaded alongside the embedded policy.script<pkg>/scripts/<filename>install_dir(if declared)Manifest-driven install paths
Artifacts with
install_dirare 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 removecan 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.
Flags:
--force- overwrite conflicts, accept signer identity changes--init- copy instruction files withplacement: "project"into the current directoryPull flow:
package.jsonmanifest (platform check, min version)install_dirwhere declaredFlow 2: Direct repository setup
The self-managed path. The package author keeps signed artifacts (with
.bundlesidecars) 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:
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:
What this path does not provide:
nono pull/nono removelifecycle managementBoth 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:
~/.claude/settings.json)nono search <query>Search the registry for packages.
nono list --installedList installed packages from the lockfile.
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
--forceis 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:
~/.config/nono/hooks/<script_name>)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.jsonartifact. 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
Known limitations
install_dirsupports~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$HOMEso 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 updateis not yet implemented.nono push- publishing is GitHub Actions only, by design.