Add a config-driven, strongly-typed secret/env resolution system to notsecrets with 11 provider backends. The system supports explicit key bindings with typed SecretRef enums and a priority chain fallback. Tier 1 providers (implemented): Env, 1Password, dotenvx, SOPS, GSM Tier 2 providers (stubbed): nuenv, direnv, mise, Bitwarden, Vault, dotenvy New types: Provider, ProviderConfig, SecretRef, SecretsConfig, SecretsError, SecretSource, EnumerableSecretSource, SecretResolver Config lives in notsecrets.toml with typed serde deserialization. ISP split: EnumerableSecretSource for backends that support bulk enumeration, SecretSource for single-key lookup only. Includes conformance test suite, 33 new tests (161 workspace total). notstrap EnvInjector migration deferred to follow-up.
notfiles
A pure-Rust dotfiles manager — and eventually, a complete new-machine bootstrap system.
notfiles started as a Rust replacement for GNU Stow. It's growing into a Cargo workspace of focused crates that together replace an entire shell-script-based dotfiles ecosystem.
Quick Start
# Symlink your dotfiles
notfiles link
# Check status
notfiles status
# Remove symlinks
notfiles unlink
On a new machine (once notstrap ships):
cargo install notstrap
notstrap run
Workspace Architecture
notfiles/
├── crates/
│ ├── notcore/ # shared types, config, paths, errors
│ ├── notfiles/ # symlink engine (stow replacement) ← you are here
│ ├── notsecrets/ # age key retrieval + sops decrypt
│ ├── nothooks/ # hook execution engine
│ └── notstrap/ # new-machine bootstrap orchestrator
├── notfiles.toml # symlink package config
└── notstrap.toml # bootstrap hook/phase config
Crate responsibilities
| Crate | Type | Does |
|---|---|---|
notcore |
lib | Shared types, config, paths, errors — no deps on other crates |
notfiles |
lib + bin | Symlink/copy packages into $HOME, track state |
notsecrets |
lib | Retrieve age key → sops decrypt → inject secrets |
nothooks |
lib + bin | Run bootstrap hooks in phases, skip already-run setup hooks |
notstrap |
bin | Orchestrate everything on a fresh machine |
Dependency graph
notstrap
├── notfiles
│ └── notcore
├── notsecrets
│ └── notcore
└── nothooks
└── notcore
notcore is the only shared dependency. No circular deps.
How notfiles Works
Each subdirectory of your dotfiles repo is a package. notfiles link walks each package and symlinks its contents into a target directory (default: $HOME), mirroring the directory structure.
dotfiles/
└── zsh/
└── .zshrc → symlink → ~/.zshrc
dotfiles/
└── git/
└── .config/
└── git/
└── config → symlink → ~/.config/git/config
State is tracked in .notfiles-state.toml so unlink and status know exactly what was linked, when, and how.
Link flow
notfiles link
│
├─ resolve_packages() discover subdirs or validate requested names
│
├─ collect_files() recursive walk, apply ignore patterns (globset)
│
├─ conflict_check() existing file? symlink to wrong target?
│
└─ linker::link_package() create symlinks (or copies), write state
State file
.notfiles-state.toml records every linked file:
[[entries]]
source = "/Users/joe/dotfiles/zsh/.zshrc"
target = "/Users/joe/.zshrc"
method = "symlink"
package = "zsh"
linked_at = "2026-03-31T10:00:00Z"
This powers status (diff expected vs actual) and unlink (clean removal with empty-parent cleanup).
New Machine Bootstrap (notstrap)
The hardest part of a new machine is the chicken-and-egg problem: you need secrets to set up the machine, but secrets live in an encrypted file that requires a key you haven't retrieved yet.
notstrap solves this with a staged bootstrap:
notstrap run
│
├─ 1. Prerequisites check
│ Is bw/sops/age available? Print exactly what's missing and stop.
│
├─ 2. notsecrets — retrieve age key
│ ├─ try: Bitwarden CLI (bw unlock)
│ ├─ fallback: --key-file <path> (USB drive)
│ └─ fallback: interactive prompt (paste key)
│ └─ sops decrypt secrets.sops.env → env injected
│ (now op, bw, github, openai, anthropic tokens are live)
│
├─ 3. Clone dotfiles repo (if not present)
│
├─ 4. notfiles link — stow all packages
│
├─ 5. nothooks --phase dot
│ shell config, git config, AI tool configs (~seconds, re-runnable)
│
├─ 6. nothooks --phase setup
│ Homebrew/Nix packages, mise runtimes, dev tools, op install (~minutes, once)
│
└─ 7. Report
✓ linked 142 files ✓ 3 dot hooks ✓ 7 setup hooks
Note: 1Password (op) is installed as a hook in phase setup — after secrets are already available via sops. It takes over secret management for day-to-day use once the machine is live.
Secrets Bootstrap Detail
notsecrets implements an AgeKeySource trait with three sources tried in order:
AgeKeySource
├── BitwardenSource bw unlock → session token → bw get item age-key
├── FileSource read from --key-file path (USB, etc.)
└── PromptSource read from stdin (paste)
Once the age key is retrieved, it's written to ~/.config/sops/age/keys.txt and sops decrypts secrets.sops.env. The decrypted file contains all critical bootstrap credentials (op, bw, github, openai, anthropic, etc.) and is injected into the environment for subsequent hooks.
Hook Phases
nothooks runs hooks in two phases defined in notstrap.toml:
| Phase | Speed | Re-runnable | Examples |
|---|---|---|---|
dot |
~seconds | Yes | shell config, git config, AI tool configs |
setup |
~minutes | No (tracked) | Homebrew packages, mise runtimes, op install |
setup hooks are tracked in .nothooks-state.toml — already-run hooks are skipped unless --force is passed. dot hooks always re-run (they're idempotent by design).
Configuration
notfiles.toml — symlink config
[defaults]
method = "symlink"
target = "~"
[packages.secrets]
method = "copy" # copy instead of symlink for sensitive files
[packages.work]
target = "~/work" # different target dir
ignore = ["*.local"]
notstrap.toml — bootstrap config
[bootstrap]
dotfiles_repo = "git@github.com:you/dotfiles.git"
dotfiles_dir = "~/dotfiles"
[[hooks]]
name = "shell-config"
script = "scripts/setup-git-config.sh"
phase = "dot"
[[hooks]]
name = "homebrew-packages"
script = "scripts/setup-packages.sh"
phase = "setup"
Migration from dotfiles/
Migration is gradual — shell scripts are replaced as Rust equivalents ship:
| Phase | Ships | Replaces |
|---|---|---|
| 1 | notfiles workspace (now) |
GNU Stow |
| 2 | notsecrets + notstrap skeleton |
setup-secrets.sh, install.sh |
| 3 | nothooks |
bootstrap.sh hook runner |
| 4 | Hook-by-hook Rust rewrites | Individual setup-*.sh scripts |
| 5 | dotfiles/ archived |
— |
Development
cargo build # build all crates
cargo test # run all tests
cargo test -p notfiles # test one crate
cargo clippy --workspace # lint everything
cargo fmt --check # format check
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.