An imperative package manager with a fixed content-addressed store, generations, and instant switching.
Grimoire is a self-contained Rust package manager that embeds Nushell to run package recipes called runes — a curated subset of the language, so recipe behavior stays stable across Nushell releases. It installs verified prebuilt packages when they exist, builds from source when they do not, and manages package catalogs as ordinary git repositories called tomes.
- Imperative runes. Package recipes are Nushell
buildfunctions — closer to PKGBUILDs or ebuilds than to a functional DSL. - Split packages. One parent build can produce several packages: companion runes claim
their slice of the output by glob (
clangis carved from thellvmmonorepo build, with the compiler-rt runtimes inside). - Decisions at plan time. Conflicts, replacements, and the transitive build-dep closure
resolve before anything is fetched or built; every mutating command takes
--dry-run. - Git-native catalogs. Tomes are git repositories you can fork, diff, pin, and update.
- Binary-first installs. Grimoire prefers a signed/checksum-verified prebuilt package and falls back to source builds when needed.
- Native machinery. Grimoire does not shell out to
git,tar,zstd,curl, ornufor its own work. - Managed build dependencies. Source builds install declared build dependencies and cache them store-only for later builds.
- Reproducible state. The lockfile records packages, versions, archive hashes, content
addresses, install reasons, holds, and tome commits;
grm generation restorerebuilds the recorded set on any install root, and--lockedoperations refuse to resolve against a tome that moved off its pinned commit. - Trustable binaries. Signed tomes authenticate source runes through a signed manifest and each published archive through its own minisign signature.
- Generations and switching. Every install/remove/upgrade creates a new generation;
grm generation switchmoves between them (back or forward) instantly without rebuilding. - Distro-citizen tooling. Install-reason tracking — removing a package takes its
now-unneeded dependencies with it in the same transaction — file-ownership queries
(
grm pkg files,grm pkg owns,grm pkg provides), preferred providers for contested commands (grm pkg prefer awk gawk), post-install notes, and tome news.
| Axis | Grimoire | Like |
|---|---|---|
| Store / install / upgrade | immutable fixed-path content-addressed store, generations, switching | Nix / Guix |
| Recipe authoring | imperative Nushell build function |
Portage / Pacman (PKGBUILD/ebuild) |
| Build-time customization | build_flags |
Portage USE flags |
| Catalogs | tomes | AUR |
| Contested commands | grm pkg prefer |
update-alternatives / eselect |
| Build / trust | managed clang/LLVM toolchain, signed binhost | Pacman / Gentoo binhost |
cargo install --git https://github.com/hermetomancy/grimoire
grm setup
grm setup --bootstrapThis installs the grm command. grm setup creates the fixed store (/grm), puts the
active profile's bin on your shell's PATH, and leaves package state alone. grm setup --bootstrap then adds the core and world tomes and installs grimoire through itself — from then
on grm upgrade grimoire is self-update.
# Add a package catalog if you did not run `grm setup --bootstrap`
grm tome add https://github.com/hermetomancy/tome-core --ref main
# Search, inspect, install
grm search hello
grm info hello
grm install hello --dry-run # the full plan: steps, build deps, migrations
grm install hello
grm ls # the linked environment; --all adds cached build deps, --explicit only your roots
# Upgrade, hold, switch generations
grm upgrade
grm pkg hold hello
grm pkg unhold hello
grm generation switch
# Ask questions
grm pkg files hello # what did this package install?
grm pkg owns ~/.grimoire/profiles/current/bin/hello # what installed this file?
grm pkg provides awk # who can provide this command?
grm pkg prefer awk gawk # pick the provider when several can
# Clean up
grm remove hello # dependencies nothing needs anymore leave with it
grm clean # prune old generations, unreferenced store paths, cachesRun grm --help for the full command tree.
Local archives are trusted only when verified: use grm install ./pkg.tar.zst --sha256 <hash>.
Passing --force installs an unverified local archive by explicit choice.
grm tome init mytome --path ./mytome
grm tome rune widget --path ./mytome
grm tome add ./mytome --ref main
grm install widget --from-source
# Build prebuilt archives
grm tome build widget --path ./mytome
grm tome build --all --path ./mytomegrm tome build writes .tar.zst archives into dist/ and records them in dist/index.nuon.
Publish by uploading dist/ to a static host and pointing tome.rn at it. A tome may also ship
announcements as news/*.md; grm tome update shows new items once and grm tome news re-reads
them.
A rune exports package metadata and a build function written in native Nushell:
export const package = {
name: "widget"
version: "1.2.3"
sources: {
main: {
url: "https://example.com/widget-1.2.3.tar.gz"
sha256: "sha256:..."
}
}
deps: {
# build-env pulls the managed toolchain (compiler, gmake, cmake, python, userland floor).
build: { default: ["build-env"] }
runtime: []
}
bins: { default: { widget: "bin/widget" } }
}
export def build [ctx] {
cd ($ctx.sources.main.dir | path join "widget-1.2.3")
./configure --prefix=($ctx.prefix)
make -j($ctx.nproc)
make install DESTDIR=($ctx.package_dir)
}The build stages into ctx.package_dir, configures against the final store prefix
(ctx.prefix), and runs in a sandboxed environment where only declared dependencies are
discoverable. See docs/rune-authoring.md for the full reference:
the ctx record, install conventions, platform conditionals, and post-install notes.
Grimoire installs into ~/.grimoire by default (override with GRIMOIRE_ROOT). The canonical
store is at /grm/store/<hash>-<name>-<version>. Active packages are surfaced through generation
profiles:
~/.grimoire/
├── store/<hash>-<name>-<version>/ # package contents
├── profiles/
│ ├── current -> gen-3 # symlink to active generation
│ └── gen-3/ # symlinks into store
│ ├── bin/
│ └── share/man/
├── state/packages/<name>.nuon # installed state
└── state/grimoire.lock.nuon # lockfile
grm setup puts ~/.grimoire/profiles/current/bin on your shell's PATH (zsh, bash, and
fish are recognised); add it manually for other shells. grm setup --bootstrap performs the
optional self-hosting bootstrap after that store/PATH setup is complete.
Official release artifacts are signed with the project minisign key:
untrusted comment: minisign public key D4CCD5A2669CAC7C
RWR8rJxmotXM1NhQBsJZQfEeWtSP+3x67Nih78Tl7An5o7UQ8gWwmTt6
Verify a release file with minisign -Vm <file> -P RWR8rJxmotXM1NhQBsJZQfEeWtSP+3x67Nih78Tl7An5o7UQ8gWwmTt6.
A tome opts into signing by declaring signers in its tome.rn (see below); the keys are
pinned on first sync and every later sync must present the same set.
Trust is per artifact, not per index: each published archive carries a detached
archive.tar.zst.minisig, and source runes are authenticated through a signed
runes-manifest.nuon (which pins every rune's content hash). The index.nuon itself is not
signed — its archive hashes are authenticated by each archive's own signature plus its checksum.
minisign -G # generate a keypair (once)
grm tome lint --path ./mytome # catch rune/manifest problems before publishing
grm tome build --all --path ./mytome # build archives into dist/
grm tome sign --path ./mytome # write & sign runes-manifest.nuon + every dist/ archivegrm tome sign generates runes-manifest.nuon from the current runes/ tree (hashing each
rune), then signs it and every dist/*.tar.zst with your minisign secret key — defaulting to
~/.minisign/minisign.key, or pass --seckey. An encrypted key prompts for its password. It
runs tome lint first and refuses to sign a tome with problems, so a broken rune can't be hashed
into the manifest and shipped with a valid signature. Commit runes-manifest.nuon and its
.minisig to the tome repo; upload the signed dist/ to your host. (You can still sign by hand
with minisign -S -m <file> if you prefer.)
Declare the public key at the manifest's top level (a signer nested under packages is
rejected as an unknown field, so a misplaced key fails loudly instead of shipping unsigned):
export const tome = {
name: "mytome"
signers: ["RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3"]
packages: {
repo: "https://example.com/mytome"
format: "http"
index: "index.nuon"
}
}Grimoire pins the key set on first sync and refuses later syncs with a missing, invalid, or rotated key.
Unsigned tomes remain valid for local/private catalogs, but remote unsigned tomes warn on add and sync because their runes and archives are not signature-verified.
Addenda are disabled while the overlay design is reworked. grm addendum ... commands currently
return an error and do not patch package metadata.
Grimoire's design eliminates many traditional package-manager risks by construction: everything is user-local, checksum-verified, optionally signature-verified, and installed without arbitrary root execution. The binding security invariants are documented in AGENTS.md §10.
Report implementation bugs in verification, extraction, or privilege boundaries privately via GitHub's vulnerability reporting tab.
