From c9fc17ecf5b6fae62c63d6681e27d08186204773 Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 20 Jul 2026 10:53:47 -0400 Subject: [PATCH 1/4] feat(git): declare the operator's commit identity (task 0015) Git identity lived only in one checkout's local configuration on one machine, so it was invisible to every other checkout and lost on a reimage. Declare it as a module instead, enabled on neogaia. It is a module rather than base plumbing so a host that should not carry a personal commit identity can decline it. --- .claude/tasks/0015-git-module.md | 28 +++++++++++++++++++++++----- CLAUDE.md | 8 +++++--- hosts/neogaia/default.nix | 1 + modules/git.nix | 25 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 modules/git.nix diff --git a/.claude/tasks/0015-git-module.md b/.claude/tasks/0015-git-module.md index 79f5a7d..c2fdb21 100644 --- a/.claude/tasks/0015-git-module.md +++ b/.claude/tasks/0015-git-module.md @@ -10,8 +10,26 @@ It is a `Module` rather than base plumbing because a `Host` that should not carr ## Acceptance criteria -- [ ] A git `Module` following the `Enable convention` exists and is enabled on `neogaia` -- [ ] The commit identity is configured through home-manager and matches the one used in existing history -- [ ] `nix flake check` builds the `neogaia` toplevel -- [ ] Manual confirmation: committing in a repository outside this checkout succeeds with no per-command identity override -- [ ] The stale note in the project's agent instructions claiming git identity is unconfigured is corrected, since commits already work here through repository-local configuration +- [x] A git `Module` following the `Enable convention` exists and is enabled on `neogaia` +- [x] The commit identity is configured through home-manager and matches the one used in existing history +- [x] `nix flake check` builds the `neogaia` toplevel +- [x] Manual confirmation: committing in a repository outside this checkout succeeds with no per-command identity override +- [x] The stale note in the project's agent instructions claiming git identity is unconfigured is corrected, since commits already work here through repository-local configuration + +## Implementation Notes + +`programs.git.userName`/`userEmail` are renamed in this home-manager pin and emit an obsolete-option trace. +The module uses `settings.user.name`/`settings.user.email`. +Do not "fix" it back. + +The commit name is the literal `"alexion"` rather than `config.user.name`, which review raised as duplication. +A Unix login and a commit display name are separate concepts that merely coincide here, so binding them would let a host overriding its login silently rewrite the operator's commit identity. + +The manual confirmation was met against the built configuration, not the running machine: `nixos-rebuild switch` needs sudo and has not run, so `~/.config/git/config` does not yet exist on `neogaia`. +The generated gitconfig was built from the `neogaia` toplevel and a commit driven under `env -i` with a scratch `HOME`, producing `alexion ` with no per-command override. +This proves the derivation rather than the deployment, and the live check remains owed at the next rebuild. + +Review surfaced an unanticipated hazard that proved harmless. +Home-manager writes `~/.config/git/config`, while an undeclared `~/.gitconfig` also exists and outranks it per key. +It holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, confirmed by re-running the commit test with both files present. +Declaring that credential helper is a reasonable follow-up, since it will not survive a reimage. diff --git a/CLAUDE.md b/CLAUDE.md index c251336..9cfec0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,9 +36,11 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla Both were true only while the machine still ran CachyOS against a distro Nix daemon. - The substituters a `nix build` fetches from are the **daemon's** (`/etc/nix/nix.conf`), *not* the `nix.settings` of the config being built — those only govern the built system. The two coincide here because the dev host runs this flake; they diverge on any machine that does not. -- Git identity is not declared in the flake — there is no `programs.git` — so it must be set by hand before the first commit on a fresh machine. - The July 2026 reimage confirmed this: it wiped the hand-written `~/.gitconfig`, and the next commit failed with `Author identity unknown`, auto-detecting `alexion@neogaia.(none)`. - It now lives in this checkout's `.git/config`, which reaches no other machine and does not survive the next reimage either; history uses `alexion `. +- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion ` — the identity all history uses — on any host enabling `modules.git`. + Once such a host has been rebuilt, a checkout on it needs no hand-written identity and keeps one across a reimage. + Two things mask a broken module, so neither is evidence it works: this checkout's `.git/config` carries the same identity, and home-manager writes `~/.config/git/config` while a `~/.gitconfig` also exists and outranks it per key. + That `~/.gitconfig` holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity today, but it is undeclared and will not survive a reimage. + Verify the module by committing in a repository outside this checkout. - The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations..config...`. - A flake only sees **git-tracked** files, so a new file that has not been `git add`ed is invisible to evaluation even though it exists on disk. The failure names the path and reads as if the file were missing: `error: Path 'secrets/shared.yaml' does not exist in Git repository`. diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index 1c04a4e..df1753f 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -36,6 +36,7 @@ modules.fish.enable = true; modules.fish.defaultShell = true; + modules.git.enable = true; modules.tmux.enable = true; modules.nvim.enable = true; modules.claude-code.enable = true; diff --git a/modules/git.nix b/modules/git.nix new file mode 100644 index 0000000..bce1c24 --- /dev/null +++ b/modules/git.nix @@ -0,0 +1,25 @@ +{ + config, + lib, + ... +}: +# git for the primary user, configured through home-manager. +let + cfg = config.modules.git; + user = config.user.name; +in +{ + options.modules.git.enable = + lib.mkEnableOption "git for the primary user, carrying the operator's commit identity"; + + config = lib.mkIf cfg.enable { + home-manager.users.${user}.programs.git = { + enable = true; + + # Git will not guess a name and address from the login and hostname. + # Without these a commit fails outright with `Author identity unknown`. + settings.user.name = "alexion"; + settings.user.email = "contact@alexion.dev"; + }; + }; +} -- 2.47.3 From 75c5745cbd2f8012abe16a92b02d649cd2576c16 Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 20 Jul 2026 11:15:15 -0400 Subject: [PATCH 2/4] feat(git): carry the commit identity on every host Default the module on rather than restating it per host, so a new host commits without remembering the line. A host that should not carry a personal identity sets enable to false. --- .claude/tasks/0015-git-module.md | 4 ++++ CLAUDE.md | 3 ++- hosts/neogaia/default.nix | 1 - modules/git.nix | 13 +++++++++++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.claude/tasks/0015-git-module.md b/.claude/tasks/0015-git-module.md index c2fdb21..2ef97cb 100644 --- a/.claude/tasks/0015-git-module.md +++ b/.claude/tasks/0015-git-module.md @@ -22,6 +22,10 @@ It is a `Module` rather than base plumbing because a `Host` that should not carr The module uses `settings.user.name`/`settings.user.email`. Do not "fix" it back. +Review on the pull request asked for the module on every host, so `enable` defaults to true rather than being restated per host, and the explicit line in `hosts/neogaia/default.nix` is gone as redundant. +Defaulting the option keeps the opt-out this task's rationale asked for, where moving the identity into the shared base config would not. +The `neogaia` toplevel hashes identically before and after the change. + The commit name is the literal `"alexion"` rather than `config.user.name`, which review raised as duplication. A Unix login and a commit display name are separate concepts that merely coincide here, so binding them would let a host overriding its login silently rewrite the operator's commit identity. diff --git a/CLAUDE.md b/CLAUDE.md index 9cfec0a..360b55f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla Both were true only while the machine still ran CachyOS against a distro Nix daemon. - The substituters a `nix build` fetches from are the **daemon's** (`/etc/nix/nix.conf`), *not* the `nix.settings` of the config being built — those only govern the built system. The two coincide here because the dev host runs this flake; they diverge on any machine that does not. -- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion ` — the identity all history uses — on any host enabling `modules.git`. +- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion ` — the identity all history uses. + It is one of the few modules defaulting to on, so a host carries it without restating it and declines by setting `modules.git.enable = false`. Once such a host has been rebuilt, a checkout on it needs no hand-written identity and keeps one across a reimage. Two things mask a broken module, so neither is evidence it works: this checkout's `.git/config` carries the same identity, and home-manager writes `~/.config/git/config` while a `~/.gitconfig` also exists and outranks it per key. That `~/.gitconfig` holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity today, but it is undeclared and will not survive a reimage. diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index df1753f..1c04a4e 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -36,7 +36,6 @@ modules.fish.enable = true; modules.fish.defaultShell = true; - modules.git.enable = true; modules.tmux.enable = true; modules.nvim.enable = true; modules.claude-code.enable = true; diff --git a/modules/git.nix b/modules/git.nix index bce1c24..4851dfc 100644 --- a/modules/git.nix +++ b/modules/git.nix @@ -9,8 +9,17 @@ let user = config.user.name; in { - options.modules.git.enable = - lib.mkEnableOption "git for the primary user, carrying the operator's commit identity"; + options.modules.git.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to configure git for the primary user, carrying the operator's + commit identity. + + On by default, so every host can commit without restating it. A host that + should not carry a personal commit identity sets this to false. + ''; + }; config = lib.mkIf cfg.enable { home-manager.users.${user}.programs.git = { -- 2.47.3 From 62eb6286b438d77fee167e86830ec3e48e867b1a Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 20 Jul 2026 12:20:13 -0400 Subject: [PATCH 3/4] feat(git): enable the module per host rather than by default Defaulting the module on hid it from a host's config, leaving no single place that lists what a host carries. Each host enables it explicitly instead, at the cost of a step when adding one. --- .claude/tasks/0015-git-module.md | 6 +++--- CLAUDE.md | 4 ++-- hosts/neogaia/default.nix | 1 + modules/git.nix | 13 ++----------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.claude/tasks/0015-git-module.md b/.claude/tasks/0015-git-module.md index 2ef97cb..5f57007 100644 --- a/.claude/tasks/0015-git-module.md +++ b/.claude/tasks/0015-git-module.md @@ -22,9 +22,9 @@ It is a `Module` rather than base plumbing because a `Host` that should not carr The module uses `settings.user.name`/`settings.user.email`. Do not "fix" it back. -Review on the pull request asked for the module on every host, so `enable` defaults to true rather than being restated per host, and the explicit line in `hosts/neogaia/default.nix` is gone as redundant. -Defaulting the option keeps the opt-out this task's rationale asked for, where moving the identity into the shared base config would not. -The `neogaia` toplevel hashes identically before and after the change. +Review on the pull request asked for the module on every host, which was first built by defaulting `enable` to true and dropping the per-host line. +The operator then chose the opposite: `enable` defaults to false and each host enables it explicitly, so a host keeps reading as a full checklist of what it carries rather than hiding a default-on module. +Enabling it is therefore a step when adding a host. The commit name is the literal `"alexion"` rather than `config.user.name`, which review raised as duplication. A Unix login and a commit display name are separate concepts that merely coincide here, so binding them would let a host overriding its login silently rewrite the operator's commit identity. diff --git a/CLAUDE.md b/CLAUDE.md index 360b55f..6d62668 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,8 +36,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla Both were true only while the machine still ran CachyOS against a distro Nix daemon. - The substituters a `nix build` fetches from are the **daemon's** (`/etc/nix/nix.conf`), *not* the `nix.settings` of the config being built — those only govern the built system. The two coincide here because the dev host runs this flake; they diverge on any machine that does not. -- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion ` — the identity all history uses. - It is one of the few modules defaulting to on, so a host carries it without restating it and declines by setting `modules.git.enable = false`. +- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion ` — the identity all history uses — on any host enabling `modules.git`. + Every new host has to enable it, so that a host reads as a full checklist of what it carries. Once such a host has been rebuilt, a checkout on it needs no hand-written identity and keeps one across a reimage. Two things mask a broken module, so neither is evidence it works: this checkout's `.git/config` carries the same identity, and home-manager writes `~/.config/git/config` while a `~/.gitconfig` also exists and outranks it per key. That `~/.gitconfig` holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity today, but it is undeclared and will not survive a reimage. diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index 1c04a4e..df1753f 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -36,6 +36,7 @@ modules.fish.enable = true; modules.fish.defaultShell = true; + modules.git.enable = true; modules.tmux.enable = true; modules.nvim.enable = true; modules.claude-code.enable = true; diff --git a/modules/git.nix b/modules/git.nix index 4851dfc..bce1c24 100644 --- a/modules/git.nix +++ b/modules/git.nix @@ -9,17 +9,8 @@ let user = config.user.name; in { - options.modules.git.enable = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether to configure git for the primary user, carrying the operator's - commit identity. - - On by default, so every host can commit without restating it. A host that - should not carry a personal commit identity sets this to false. - ''; - }; + options.modules.git.enable = + lib.mkEnableOption "git for the primary user, carrying the operator's commit identity"; config = lib.mkIf cfg.enable { home-manager.users.${user}.programs.git = { -- 2.47.3 From 4ecb86052b60dca176e8e5f7c0e7c8e2c7746f7c Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 20 Jul 2026 12:22:36 -0400 Subject: [PATCH 4/4] docs(git): record the verified deployment (task 0015) Also note that `git config --global` lists only ~/.gitconfig and is not a view of what git resolves, which misreads as the module's file being ignored. --- .claude/tasks/0015-git-module.md | 13 ++++++++----- CLAUDE.md | 12 ++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.claude/tasks/0015-git-module.md b/.claude/tasks/0015-git-module.md index 5f57007..d824442 100644 --- a/.claude/tasks/0015-git-module.md +++ b/.claude/tasks/0015-git-module.md @@ -29,11 +29,14 @@ Enabling it is therefore a step when adding a host. The commit name is the literal `"alexion"` rather than `config.user.name`, which review raised as duplication. A Unix login and a commit display name are separate concepts that merely coincide here, so binding them would let a host overriding its login silently rewrite the operator's commit identity. -The manual confirmation was met against the built configuration, not the running machine: `nixos-rebuild switch` needs sudo and has not run, so `~/.config/git/config` does not yet exist on `neogaia`. -The generated gitconfig was built from the `neogaia` toplevel and a commit driven under `env -i` with a scratch `HOME`, producing `alexion ` with no per-command override. -This proves the derivation rather than the deployment, and the live check remains owed at the next rebuild. +The manual confirmation is met on the running machine. +The operator rebuilt `neogaia`, `~/.config/git/config` is now a home-manager symlink, and a commit in a repository outside this checkout was authored `alexion ` in the real environment with no per-command override and no identity in the test repository's own config. Review surfaced an unanticipated hazard that proved harmless. -Home-manager writes `~/.config/git/config`, while an undeclared `~/.gitconfig` also exists and outranks it per key. -It holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, confirmed by re-running the commit test with both files present. +Home-manager writes `~/.config/git/config`, while an undeclared `~/.gitconfig` also exists and outranks it on any key set in both. +It holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, confirmed against the deployed configuration. Declaring that credential helper is a reasonable follow-up, since it will not survive a reimage. + +This checkout's `.git/config` still sets the same identity, now redundant. +Removing it would let the module govern here too, so a future breakage surfaces instead of being masked. +It is local, untracked state, so it is left alone rather than changed as part of this task. diff --git a/CLAUDE.md b/CLAUDE.md index 6d62668..6a2a405 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,10 +38,14 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla The two coincide here because the dev host runs this flake; they diverge on any machine that does not. - Git identity is declared in the flake by `modules/git.nix`, which writes `alexion ` — the identity all history uses — on any host enabling `modules.git`. Every new host has to enable it, so that a host reads as a full checklist of what it carries. - Once such a host has been rebuilt, a checkout on it needs no hand-written identity and keeps one across a reimage. - Two things mask a broken module, so neither is evidence it works: this checkout's `.git/config` carries the same identity, and home-manager writes `~/.config/git/config` while a `~/.gitconfig` also exists and outranks it per key. - That `~/.gitconfig` holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity today, but it is undeclared and will not survive a reimage. - Verify the module by committing in a repository outside this checkout. + It is deployed on `neogaia` and verified: a commit in a repository outside this checkout is authored `alexion ` with no override. + Verify it that way rather than from this checkout, whose `.git/config` carries the same identity and would mask a broken module. + Home-manager writes `~/.config/git/config`, and `~/.gitconfig` is a second global file that git also reads, outranking it on any key set in both. + `~/.gitconfig` currently holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, but it is undeclared and will not survive a reimage. +- `git config --global` is a listing and writing filter over `~/.gitconfig` alone, **not** a view of what git resolves. + With both global files present it prints only `~/.gitconfig`, which reads as proof that `~/.config/git/config` is being ignored entirely. + It is not: drop `--global` and both files appear, each key resolving to the last file that sets it. + A `git config --global ` write also lands in `~/.gitconfig`, the file that outranks the flake-managed one. - The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations..config...`. - A flake only sees **git-tracked** files, so a new file that has not been `git add`ed is invisible to evaluation even though it exists on disk. The failure names the path and reads as if the file were missing: `error: Path 'secrets/shared.yaml' does not exist in Git repository`. -- 2.47.3