Skip to content

Wrap ragenix's required flags in just recipes - #113

Merged
alycda merged 6 commits into
mainfrom
just-secret-recipes
Aug 22, 2026
Merged

Wrap ragenix's required flags in just recipes#113
alycda merged 6 commits into
mainfrom
just-secret-recipes

Conversation

@alycda

@alycda alycda commented Aug 22, 2026

Copy link
Copy Markdown
Owner

ragenix -e secrets/personal/foo.age fails from anywhere in this repo, in two different ways that both look worse than they are:

  • From the repo root: ./secrets.nix does not exist!--rules defaults to ./secrets.nix, and ours lives in secrets/.
  • From secrets/: No usable identity or identities — the CLI hunts for SSH keys in ~/.ssh, which the dev container doesn't have. age.identityPaths in home-manager/modules/git.nix is read by the agenix module at activation time; the ragenix binary never sees it.

So both flags have to be supplied by hand on every invocation, and omitting -i reports a missing key rather than a missing flag — which sends you hunting for a lost identity instead of re-reading the command.

What this adds

just edit-secret personal/git-config.age   # or secrets/personal/git-config.age
just rekey-secrets
  • _age-identity resolves ~/.age/personal-key.txt (override with $RAGENIX_IDENTITY) and, when it's missing, prints the docker cp recovery line the container entrypoint already suggests — or names the override, when that's what's pointing at nothing.
  • edit-secret takes the path in any of the four forms you'd plausibly type or tab-complete: bare (personal/x.age), repo-relative (secrets/personal/x.age), ./-prefixed, or absolute.
  • rekey-secrets wraps --rekey, which needs the same two flags. It's the rarer operation — recipient changes, key rotation — which is exactly when you won't have the invocation in shell history.

Also carries a content update to the private agent-instruction overlay (secrets/personal/agent-instructions.age), which is what edit-secret got used for first.

Review round

Three follow-up commits, one per finding, all in error paths rather than the happy path:

  • --editor is required, not optional. It advertises [env: EDITOR=...] in --help, but clap treats it as a required argument that merely defaults from the environment — so an unset EDITOR died on argument parsing with a usage dump. That's the same class of confusion the recipe exists to remove, and it fires wherever the session env is thin (non-interactive shells, or the failed-activation state docker/CLAUDE.md describes). Now passed explicitly, defaulting to hx.
  • The path was interpolated as raw text. just substitutes into the recipe body rather than passing arguments, so just edit-secret 'x.age"; echo INJECTED; :"' executed the injected command — and any path with a space broke for the same reason. Fixed with quote(). Stripping only a literal leading secrets/ also meant the ./ and absolute forms got a second secrets/ glued on; both are handled now.
  • The recovery hint ignored $RAGENIX_IDENTITY. It reported the path it actually looked at, then told you to place a key at the default path — useless when your own override is the broken one.

There's deliberately still no check that the target file exists: ragenix -e on a non-existent path is how a new secret gets created, so ragenix's rules lookup stays the gate. It also rejects a file that exists but was never listed in secrets.nix, which an existence check wouldn't.

Testing

  • All four path spellings, from both the repo root and from inside secrets/: ragenix reports "wasn't changed, skipping re-encryption" and the tree stays clean.
  • EDITOR unset now gets past argument parsing and launches the editor.
  • The injection case no longer executes — the path reaches ragenix literally and is refused by the rules lookup.
  • A missing identity fails through _age-identity with the right hint for each case (default vs. override) and a non-zero exit.
  • rekey-secrets against all four real secrets: the armored encoding secrets.nix depends on is preserved, and every result still decrypts. That churn was reverted — rekeying rewrites ciphertext every run, so committing it would be noise.
  • The overlay update was checked to decrypt and stay armored before it was committed.

Worth knowing

rekey-secrets rewrites all four secrets even when the recipient list hasn't changed, so its diff can't be reviewed by inspection — only by decrypting. That's inherent to ragenix --rekey, not something the recipe introduces.

alycda added 3 commits August 22, 2026 18:18
`ragenix -e secrets/personal/foo.age` fails from anywhere in this repo, for
two unrelated reasons that both look like something worse than they are:

- Run from the repo root it dies with "./secrets.nix does not exist!" —
  `--rules` defaults to ./secrets.nix and ours lives in secrets/.
- Run from secrets/ it dies with "No usable identity or identities" — the
  CLI hunts for SSH keys in ~/.ssh, which the dev container doesn't have.
  `age.identityPaths` in home-manager/modules/git.nix is consumed by the
  agenix *module* at activation time; the ragenix binary never reads it.

So every edit needs two flags supplied by hand, and omitting -i reports a
missing key rather than a missing flag — which sends you looking for a lost
identity instead of re-reading the command. Bake both into a recipe.

- `_age-identity` resolves ~/.age/personal-key.txt (overridable via
  $RAGENIX_IDENTITY), and when it's absent prints the `docker cp` recovery
  line the container entrypoint already suggests. Split out rather than
  inlined because rekeying needs the same answer.
- `edit-secret` accepts the path as typed from either the repo root or from
  inside secrets/, since `just` runs recipes from the justfile directory and
  the two spellings are equally natural.

Also terminates the file with a newline, which it was missing.
`ragenix --rekey` needs `--rules` and `-i` exactly as editing does, and fails
the same misleading way without them. It's the rarer operation — recipient
list changes, key rotation — which is precisely when you won't have the
invocation in shell history, so it's the one more worth wrapping.

Reuses `_age-identity`, so the key location and its recovery hint stay in one
place.

Verified on the real secrets: rekeying all four preserves the armored
encoding secrets.nix depends on (binary blobs don't reliably survive the trip
into a commit), and every result still decrypts with the same identity. The
test churn was reverted — ciphertext changes on every rekey, so committing it
would be noise.
Content-only change: the recipient list in secrets/secrets.nix is untouched,
so this is an edit rather than a rekey. Verified before committing that it
still decrypts with the personal identity and that the armored encoding
survived — secrets.nix notes that binary blobs don't reliably make it into a
commit, and this file is now the first one edited through `just edit-secret`.

What changed inside is deliberately not described here. The overlay exists so
that layer is only ever ciphertext in this repo; restating it in a commit
message would defeat that.
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

⊕ Entity-level changes

justfile

Status Type Name
- chunk lines 101-115
+ chunk lines 101-120
+ chunk lines 121-140
+ chunk lines 141-160
+ chunk lines 161-175

secrets/personal/agent-instructions.age

Status Type Name
- chunk lines 1-15
+ chunk lines 1-20
+ chunk lines 21-24

Summary: 6 added, 2 deleted across 2 files

functions and classes, not lines · sem

@alycda
alycda marked this pull request as ready for review August 22, 2026 19:19
alycda added 3 commits August 22, 2026 19:25
`--editor` looks optional in `ragenix --help` because it advertises `[env:
EDITOR=...]`, but clap treats it as a required argument that merely *defaults*
from the environment. With EDITOR unset the recipe dies on argument parsing:

    error: the following required arguments were not provided:
      --editor <EDITOR>

That's the same trap the recipe was written to close — it reads as a broken
recipe rather than an unset variable. It shows up wherever the session env is
thin: a non-interactive shell, or the failed-activation state docker/CLAUDE.md
describes, where the prompt works because dotfiles linked but the packages and
their env never arrived.

Pass it explicitly and default to hx, matching the $EDITOR the home-manager
profile sets anyway.
just's interpolation is raw text substitution into the recipe body, not
argument passing, so the path escaped its own quotes:

    just edit-secret 'x.age"; echo INJECTED; :"'   # printed INJECTED

Self-inflicted in a personal justfile rather than a real attack surface, but a
path with a space in it broke for the same reason, and `quote()` fixes both.

While here, honour the claim the comment above it makes. Stripping only a
literal leading `secrets/` meant the two forms a shell completion actually
produces — `./secrets/personal/x.age` and the absolute path — both got a
second `secrets/` glued on and died in ragenix's rules lookup. Strip `./` and
a `$PWD/` prefix first.

Deliberately still no check that the file exists: `ragenix -e` on a
non-existent path is how a new secret is created, so ragenix's rules lookup
stays the gate — it also rejects a file that exists but was never listed in
secrets.nix, which an existence check wouldn't.
`_age-identity` reported the path it actually looked at, then unconditionally
told you to put a key at ~/.age/personal-key.txt. With $RAGENIX_IDENTITY set
to a moved or mistyped path, following that instruction verbatim leaves the
recipe failing identically, because the override still wins.

Name the variable instead when it's the one in play. The docker cp hint is
still right for the default case, which is the common one.
@alycda
alycda merged commit a60b8a9 into main Aug 22, 2026
3 checks passed
@alycda
alycda deleted the just-secret-recipes branch August 22, 2026 19:33
alycda added a commit that referenced this pull request Aug 22, 2026
Edited on top of main's current ciphertext, after merging #113 in — the copy
on this branch was still the pre-#113 blob, and editing that would have
silently reverted the overlay update that landed with it.

Recipients in secrets/secrets.nix are unchanged, so this is an edit, not a
rekey. Verified before committing that it still decrypts with the personal
identity and kept its armored encoding.

Contents deliberately not described: the overlay exists so that layer is only
ever ciphertext in this repo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant