Compare commits
6 Commits
431f75aad7
...
7711b841dd
| Author | SHA1 | Date | |
|---|---|---|---|
| 7711b841dd | |||
| 66582b498c | |||
| 54c191f801 | |||
| d0bfc7b21b | |||
| e5d8f69f16 | |||
| 42c602b814 |
@@ -22,40 +22,38 @@ One thing must be true before the disk is erased: every branch worth keeping has
|
|||||||
|
|
||||||
- [x] The `Host`'s disk layout declares a 2 GiB EFI system partition
|
- [x] The `Host`'s disk layout declares a 2 GiB EFI system partition
|
||||||
- [x] Every local branch worth keeping exists on the remote before the disk is erased
|
- [x] Every local branch worth keeping exists on the remote before the disk is erased
|
||||||
- [ ] The reimage is performed from a configuration carrying the hardware profile, following the existing install documentation
|
- [x] The reimage is performed from a configuration carrying the hardware profile, following the existing install documentation
|
||||||
- [ ] The install documentation is corrected wherever the procedure diverged from what it describes
|
- [x] The install documentation is corrected wherever the procedure diverged from what it describes
|
||||||
- [ ] Manual confirmation: the machine boots, the encrypted root unlocks, and console login succeeds
|
- [x] Manual confirmation: the machine boots, the encrypted root unlocks, and console login succeeds
|
||||||
- [ ] Manual confirmation: reported free space on the boot partition is consistent with its 2 GiB size, resolving the discrepancy observed before the reimage — where a 512 MiB partition reported 1022 MiB
|
- [x] Manual confirmation: reported free space on the boot partition is consistent with its 2 GiB size, resolving the discrepancy observed before the reimage — where a 512 MiB partition reported 1022 MiB
|
||||||
- [x] The project's agent instructions record that a flake only sees git-tracked files, so an untracked file is invisible to evaluation
|
- [x] The project's agent instructions record that a flake only sees git-tracked files, so an untracked file is invisible to evaluation
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
This task is **blocked on an operator action**, not finished. Three of seven
|
Done. The reimage was performed by the operator and the machine now runs the
|
||||||
criteria are satisfied: the declaration, the branch check, and the recorded
|
configuration this repository declares.
|
||||||
gotcha. The remaining four all depend on erasing the disk, which is not an
|
|
||||||
action taken on the operator's behalf.
|
|
||||||
|
|
||||||
The branch check was verified rather than assumed — no local branch holds a
|
The install ran clean: the operator reports no step diverged from
|
||||||
commit absent from the remote, so nothing is lost to the wipe. Two items that
|
`docs/install.md`, so criterion 4 is satisfied with no further corrections. The
|
||||||
live outside the repo do not survive it and are not covered by any criterion:
|
four corrections that landed earlier came from reading the procedure; the run
|
||||||
the agent memory directory, and the wifi credentials.
|
itself found nothing to add. That is the reproducibility evidence the task was
|
||||||
|
after — the document is a procedure, not a record of one improvised session.
|
||||||
|
|
||||||
Criterion 4 is deliberately left open despite four corrections already landing
|
Verified on the running machine rather than assumed:
|
||||||
on the main branch — a wrong repository name in both clone commands, a closing
|
|
||||||
section describing a superseded key-derivation design, a stale enumeration of
|
|
||||||
flake inputs, and a bootstrap-ordering sentence contradicting a later one. All
|
|
||||||
four were found by reading the procedure. The criterion asks for divergences
|
|
||||||
found by *running* it, which has not happened. The wrong repository name would
|
|
||||||
have stopped the install at the clone step, so the reading pass was worth doing;
|
|
||||||
it is just not the same evidence.
|
|
||||||
|
|
||||||
**Ordering hazard.** Between merging this and completing the reimage, the
|
- `/dev/nvme0n1p1` is 2.0 GiB and `df` reports 2.0 GiB. The pre-reimage
|
||||||
repository asserts a partition layout the physical disk does not have, and disko
|
discrepancy, where a 512 MiB partition reported 1022 MiB, is gone.
|
||||||
reconciles nothing on a running machine. The declaration should reach the branch
|
- The hardware profile is live — `mem_sleep_default=deep` is on the kernel
|
||||||
the install reads immediately before the install, not days ahead of it. If the
|
command line and `psmouse` is blacklisted and not loaded.
|
||||||
reimage is deferred, this is the file that quietly lies about the only laptop.
|
- `cryptroot` is open on `nvme0n1p2` with btrfs mounted, reached through a
|
||||||
|
console login, so the boot-unlock-login path is exercised end to end.
|
||||||
|
- One generation exists (`system-1-link`), confirming a fresh install rather
|
||||||
|
than a rebuild of the prior system.
|
||||||
|
|
||||||
One correction outside this task's scope also landed here: the gotcha claiming
|
The ordering hazard closed favourably: the declaration and the install landed
|
||||||
git identity was unconfigured, which a hand-written `~/.gitconfig` had made
|
close enough together that the repository never asserted a layout the disk
|
||||||
false. Moving that identity into the flake belongs to the commit-identity work
|
lacked for long.
|
||||||
this task's description explicitly fences off.
|
|
||||||
|
Two items outside the repo did not survive the wipe, as anticipated, and neither
|
||||||
|
is covered by a criterion: the wifi credentials, and the agent memory directory
|
||||||
|
— confirmed empty after the reimage.
|
||||||
|
|||||||
@@ -14,7 +14,27 @@ Generating the scan requires root on the target machine.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] The detection file's contents come from a scan of the running machine rather than a guess
|
- [x] The detection file's contents come from a scan of the running machine rather than a guess
|
||||||
- [ ] Filesystem and swap declarations are absent from it, leaving the disk layout as the sole source of those
|
- [x] Filesystem and swap declarations are absent from it, leaving the disk layout as the sole source of those
|
||||||
- [ ] The file no longer describes itself as a placeholder, and says plainly what it holds
|
- [x] The file no longer describes itself as a placeholder, and says plainly what it holds
|
||||||
- [ ] `nix flake check` builds the `neogaia` toplevel
|
- [x] `nix flake check` builds the `neogaia` toplevel
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
**The guess was wider than the measurement, not narrower.**
|
||||||
|
It named `thunderbolt`, `usb_storage`, and `sd_mod`, none of which the scan reports; the scan adds `rtsx_pci_sdmmc` for the card reader.
|
||||||
|
Nothing needed to reach the root device was missing, so the guess was adequate as the task assumed, but it was not accurate.
|
||||||
|
`sd_mod` survives in the resolved list regardless, supplied by nixpkgs' own defaults; `thunderbolt` and `usb_storage` now genuinely go, and they matter only for booting from external media, which this machine does not do.
|
||||||
|
|
||||||
|
**Two further lines from the scan were dropped beyond the filesystem and swap declarations the task named.**
|
||||||
|
`boot.initrd.luks.devices."cryptroot".device` is derived by the disk layout, which the layout file already states, so keeping it would have created the same duplicate definition the task drops the filesystems to avoid.
|
||||||
|
`hardware.cpu.intel.updateMicrocode` falls outside the three things the task keeps, and the hardware profile supplies it anyway.
|
||||||
|
Both were checked rather than assumed: after the change the LUKS device, all four filesystems, and microcode all still resolve.
|
||||||
|
|
||||||
|
**The header was rewritten twice.**
|
||||||
|
Its first form enumerated the file's three attributes, which the repo's comment convention names as a feature inventory and forbids in a file-top header.
|
||||||
|
It now carries provenance and the absence pointer only.
|
||||||
|
|
||||||
|
**Verified by a boot.**
|
||||||
|
`nix flake check` proves only that the configuration evaluates and builds, so the reduced initrd was exercised on the machine: it unlocked LUKS and mounted the btrfs root unaided.
|
||||||
|
The running system's store path matches this configuration's build exactly, confirming the boot used it rather than an earlier generation.
|
||||||
|
|||||||
64
.claude/tasks/0018-agent-sudo-credential-caching.md
Normal file
64
.claude/tasks/0018-agent-sudo-credential-caching.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
## What to build
|
||||||
|
|
||||||
|
Make root-requiring work reachable from an agent session without waiving the password, by sharing sudo's credential cache across sessions and failing loudly when it is cold.
|
||||||
|
|
||||||
|
Sudo caches an authentication for a timeout window, but keys that cache by terminal under its default `timestamp_type=tty`.
|
||||||
|
An agent's commands run in subprocesses on a different terminal, so a `sudo -v` typed in the operator's shell is invisible to them and every privileged command fails.
|
||||||
|
Setting `timestamp_type=global` keys the cache per user instead, so one authentication covers the whole machine for the window.
|
||||||
|
The timeout is raised to 60 minutes so a session needing root authenticates once rather than every five.
|
||||||
|
|
||||||
|
No `NOPASSWD` rule is introduced, and this is the point of the design.
|
||||||
|
The password remains genuinely required; only its cache is shared.
|
||||||
|
A `NOPASSWD` entry for `nixos-rebuild` would be indistinguishable from blanket root on this machine, since anything able to edit the flake and then rebuild it owns the system.
|
||||||
|
|
||||||
|
The tradeoff is real and bounded: during the window, any process running as the operator can use the cached credential, not only the agent.
|
||||||
|
That is acceptable on a single-user personal laptop where the agent already runs as that user, and it is the reason this belongs to a laptop rather than to any future server `Host`.
|
||||||
|
|
||||||
|
The second half is failure behaviour.
|
||||||
|
A cold cache today surfaces as a bare non-zero exit with no output, which reads as an unexplained stall: the operator has to notice the agent is stuck and then work out what it wanted.
|
||||||
|
A `PreToolUse` hook probing `sudo -n true` turns that into an immediate, actionable refusal naming the command to run.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [-] `timestamp_type=global` and a 60-minute timeout are declared as plumbing in the shared base config
|
||||||
|
- [x] No `NOPASSWD` rule is introduced, and the wheel group still requires a password
|
||||||
|
- [x] Confirmed that NixOS does not already set `timestamp_type` elsewhere, so the declaration is not silently overridden
|
||||||
|
- [x] A `PreToolUse` hook in the claude-code `Module` denies a privileged command when the cache is cold, naming `sudo -v` in its message
|
||||||
|
- [x] The hook's behaviour is correct when the harness sandbox, rather than a cold cache, is what blocks the command
|
||||||
|
- [x] `nix flake check` builds the `neogaia` toplevel
|
||||||
|
- [x] Manual confirmation after a rebuild: `sudo -v` in one terminal lets a privileged command succeed from an agent session, and that command fails with the hook's message once the window lapses
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
**The sudo settings are declared in the claude-code module, not in the shared base config.**
|
||||||
|
The criterion asking for the shared base contradicted this task's own rationale, which argues the widened cache suits a single-user machine and should not reach a future server.
|
||||||
|
Neither placement was right, though: the setting and the hook that depends on it belong together.
|
||||||
|
The hook reads the credential cache from a process of its own, which only works under `timestamp_type=global`, so a host enabling the module without the sudo half would get a hook that never sees a cached credential and refuses every privileged command permanently.
|
||||||
|
Declaring both under the module's `enable` makes that impossible to get wrong, and carries the setting to any future host that runs the agent.
|
||||||
|
|
||||||
|
The cost is that enabling a developer tool now changes the machine's sudo posture, which a reader auditing sudo policy would not expect to find there.
|
||||||
|
The enable option's description carries the warning so it surfaces in generated documentation.
|
||||||
|
Should a host ever need the agent without the widened cache, that is when a separate sub-option earns its place; adding one now would be speculative.
|
||||||
|
|
||||||
|
**Verified against the running system, in both cache states.**
|
||||||
|
Cold, the hook refuses with its message; warm, it permits and the command uses a credential authenticated in a different terminal.
|
||||||
|
The hook process is not itself sandboxed, so it reads the real cache rather than refusing unconditionally — the failure mode that would have required it to fail open instead.
|
||||||
|
|
||||||
|
One residual is worth knowing.
|
||||||
|
The hook governs whether a privileged command is attempted, not whether it can run: the agent's own sandbox blocks `sudo` separately, and swallows it into a bare exit with no output.
|
||||||
|
A permitted command can therefore still fail for that unrelated reason, and needs the sandbox disabled.
|
||||||
|
The two are distinguishable in practice, since only one of them produces the hook's message.
|
||||||
|
|
||||||
|
**The operator must authenticate from a real terminal.**
|
||||||
|
Warming the cache from inside an agent session does not work: that shell has no controlling terminal, so sudo cannot prompt and reports `a terminal is required to read the password`.
|
||||||
|
Feeding the password by another route was rejected rather than unexplored.
|
||||||
|
Reading it from the agent's stdin would route it through the agent, and an askpass helper on this console-only machine could only prompt on the pane the agent already draws to, which trains the operator to type a password into an agent-controlled surface.
|
||||||
|
A separate terminal is the only safe channel, which is precisely what the global cache keying exists to make useful.
|
||||||
|
|
||||||
|
**`jq` is now a home package.**
|
||||||
|
The hook parses the tool input handed to it on stdin, and nothing on the profile provided a JSON parser.
|
||||||
|
Matching on the raw JSON text with `grep` was rejected: a command containing quotes or newlines would break it, and this hook fails closed, so a parsing mistake blocks real work.
|
||||||
|
|
||||||
|
**The sudo detection is anchored to command position.**
|
||||||
|
`grep sudo /etc/passwd` and `echo "run sudo -v"` are allowed; `sudo x`, `cd /tmp && sudo x`, and `true; sudo x` are blocked.
|
||||||
|
A `sudo` inside a quoted string that happens to sit in command position will still trip the guard, which errs toward asking rather than stalling.
|
||||||
21
CLAUDE.md
21
CLAUDE.md
@@ -29,8 +29,9 @@ 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.
|
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 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.
|
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` — but commits do work: identity comes from a hand-written `~/.gitconfig` plus this checkout's `.git/config`.
|
- 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.
|
||||||
Both sit outside the flake, so neither survives a reimage nor reaches another machine; history uses `alexion <contact@alexion.dev>`.
|
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 <contact@alexion.dev>`.
|
||||||
- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.<host>` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations.<host>.config...`.
|
- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.<host>` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations.<host>.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.
|
- 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`.
|
The failure names the path and reads as if the file were missing: `error: Path 'secrets/shared.yaml' does not exist in Git repository`.
|
||||||
@@ -39,15 +40,29 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
|||||||
That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source; the tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays.
|
That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source; the tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays.
|
||||||
- The remote is self-hosted Gitea (`git.alexion.dev`), and the intended CLI is `gitea-axi` rather than `tea`.
|
- The remote is self-hosted Gitea (`git.alexion.dev`), and the intended CLI is `gitea-axi` rather than `tea`.
|
||||||
`gitea-axi` resolves the repository from the `origin` remote and takes credentials from the `axi` tea login, so both are implicit inside a checkout.
|
`gitea-axi` resolves the repository from the `origin` remote and takes credentials from the `axi` tea login, so both are implicit inside a checkout.
|
||||||
**None of it is installed on the NixOS build.** No `gitea-axi`, no `tea`, no `gh`, no tea login under `~/.config/tea`, and no `GITEA_*` environment — the flake names `gitea-axi` only in the claude-code module's permissions and never packages it.
|
**None of it is installed on the NixOS build.** No `gitea-axi`, no `tea`, no `gh`, no tea login under `~/.config/tea`, and no `GITEA_*` environment — the flake names `gitea-axi` only as the claude-code module's `SessionStart` hook command and never packages it, so that hook invokes a binary that is not on `PATH`.
|
||||||
Pull requests therefore cannot be opened from this machine until a module provides the tool and its credentials; branches can only be pushed.
|
Pull requests therefore cannot be opened from this machine until a module provides the tool and its credentials; branches can only be pushed.
|
||||||
The earlier claim that `tea` remains installed described the machine while it still ran CachyOS with these tools installed by hand.
|
The earlier claim that `tea` remains installed described the machine while it still ran CachyOS with these tools installed by hand.
|
||||||
- `~/.claude/skills` is generated by home-manager with `recursive = true`, so the directories are real and writable but every leaf file is a read-only symlink into the store.
|
- `~/.claude/skills` is generated by home-manager with `recursive = true`, so the directories are real and writable but every leaf file is a read-only symlink into the store.
|
||||||
Editing a skill in place fails; its source is `modules/claude-code/skills/<name>/` here, applied by a rebuild.
|
Editing a skill in place fails; its source is `modules/claude-code/skills/<name>/` here, applied by a rebuild.
|
||||||
Creating a new file under `~/.claude/skills/` succeeds silently and is the trap — it stays outside the repo and reaches no other machine.
|
Creating a new file under `~/.claude/skills/` succeeds silently and is the trap — it stays outside the repo and reaches no other machine.
|
||||||
Copying out of that tree needs `cp -rL` plus `chmod -R u+w`: a plain `cp -r` copies the symlinks, putting store paths into the destination, and dereferenced files keep the store's read-only mode.
|
Copying out of that tree needs `cp -rL` plus `chmod -R u+w`: a plain `cp -r` copies the symlinks, putting store paths into the destination, and dereferenced files keep the store's read-only mode.
|
||||||
|
- `home-manager.users.<user>.home.file` is keyed by **absolute** path, not by a path relative to the home directory.
|
||||||
|
Evaluating `home.file.".claude/CLAUDE.md"` fails with "does not provide attribute"; the working key is `home.file."/home/alexion/.claude/CLAUDE.md"`.
|
||||||
|
List the real keys with `nix eval --json .#nixosConfigurations.<host>.config.home-manager.users.<user>.home.file --apply builtins.attrNames` rather than guessing one.
|
||||||
|
A key's `.source` is the input file, whose store path differs from the deployed symlink's target (home-manager copies it to a `hm_`-prefixed path) even though the contents match.
|
||||||
- nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`); the config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`.
|
- nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`); the config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`.
|
||||||
- nixpkgs `vimPlugins.nvim-treesitter` tracks the rewritten `main` branch: there is no `require("nvim-treesitter.configs").setup{ensure_installed,highlight,indent}`. Under nixvim, use `plugins.treesitter` with `highlight.enable`/`indent.enable` and `grammarPackages = with config.programs.nixvim.plugins.treesitter.package.builtGrammars; [ ... ]` — the module's own `package.builtGrammars`, **not** `pkgs.vimPlugins.nvim-treesitter.*` (whose query files can mismatch). The module targets the main branch and enables features via neovim-native APIs (`vim.treesitter.start()`, `require'nvim-treesitter'.indentexpr()`).
|
- nixpkgs `vimPlugins.nvim-treesitter` tracks the rewritten `main` branch: there is no `require("nvim-treesitter.configs").setup{ensure_installed,highlight,indent}`. Under nixvim, use `plugins.treesitter` with `highlight.enable`/`indent.enable` and `grammarPackages = with config.programs.nixvim.plugins.treesitter.package.builtGrammars; [ ... ]` — the module's own `package.builtGrammars`, **not** `pkgs.vimPlugins.nvim-treesitter.*` (whose query files can mismatch). The module targets the main branch and enables features via neovim-native APIs (`vim.treesitter.start()`, `require'nvim-treesitter'.indentexpr()`).
|
||||||
- Neovim is configured via **nixvim** (flake input `nixvim`, consumed as `inputs.nixvim.homeModules.nixvim` added to `home-manager.sharedModules`, config under `home-manager.users.<user>.programs.nixvim`). `nixvim.inputs.nixpkgs.follows = "nixpkgs"` is set; nixvim then emits a benign eval warning that its pinned nixpkgs differs from the followed one — builds and runs fine, do not "fix" it by dropping the follows.
|
- Neovim is configured via **nixvim** (flake input `nixvim`, consumed as `inputs.nixvim.homeModules.nixvim` added to `home-manager.sharedModules`, config under `home-manager.users.<user>.programs.nixvim`). `nixvim.inputs.nixpkgs.follows = "nixpkgs"` is set; nixvim then emits a benign eval warning that its pinned nixpkgs differs from the followed one — builds and runs fine, do not "fix" it by dropping the follows.
|
||||||
- To reference the nixvim-built package's own attrs (e.g. treesitter `builtGrammars`) inside our NixOS module, give `home-manager.users.<user>` the module-function form (`hm: { programs.nixvim = { ... hm.config.programs.nixvim... }; }`), since the outer `config` is the NixOS config, not the home-manager one.
|
- To reference the nixvim-built package's own attrs (e.g. treesitter `builtGrammars`) inside our NixOS module, give `home-manager.users.<user>` the module-function form (`hm: { programs.nixvim = { ... hm.config.programs.nixvim... }; }`), since the outer `config` is the NixOS config, not the home-manager one.
|
||||||
|
- The agent's Bash sandbox blocks `sudo` and swallows it into a bare exit 1 with **no stderr**, which looks identical to the command itself failing.
|
||||||
|
Re-run with the sandbox disabled to see the real error (`sudo: a password is required`) before diagnosing anything else.
|
||||||
|
Separately, `nixos-generate-config --show-hardware-config` needs root on this machine even just to print: unprivileged it dies at `Failed to retrieve subvolume info for /`, because the root filesystem is btrfs.
|
||||||
|
- Sudo's credential cache is keyed per user rather than per terminal (`timestamp_type=global`, 60-minute window, declared by the claude-code module), so an authentication made in one terminal counts for commands the agent runs.
|
||||||
|
Warming it with `sudo -v` through the agent's own shell — including the `!` prefix — never works: that shell has no controlling terminal, and sudo reports `a terminal is required to read the password`.
|
||||||
|
It has to be a separate terminal.
|
||||||
|
A `PreToolUse` hook refuses privileged commands while the cache is cold, so a cold cache announces itself instead of stalling; a failure *without* that message is the sandbox, not the cache.
|
||||||
|
- `home-manager.users.<user>` cannot be assigned twice at the same level in one module: `home-manager.users.${user}.home.packages` alongside `home-manager.users.${user}.programs.x` fails with `error: dynamic attribute 'alexion' already defined`.
|
||||||
|
The interpolated key makes it a dynamic attribute, which nix will not merge the way it merges static paths.
|
||||||
|
Nest both under a single `home-manager.users.${user} = { ... }`.
|
||||||
- **Verifying a nixvim change headless:** `programs.nixvim.build.package`'s wrapper has **no `-u`**, so running `$OUT/bin/nvim` loads the caller's `~/.config/nvim` (the dev host's real config), *not* the built config — silently. To exercise the built config, launch with `-u "$(nix build --no-link --print-out-paths .#…programs.nixvim.build.initFile)"` and a scratch `HOME`/`XDG_CONFIG_HOME`. `conceallevel` is window-local: set it with `opt_local`/`vim.wo`, never `vim.bo[buf]` (which errors).
|
- **Verifying a nixvim change headless:** `programs.nixvim.build.package`'s wrapper has **no `-u`**, so running `$OUT/bin/nvim` loads the caller's `~/.config/nvim` (the dev host's real config), *not* the built config — silently. To exercise the built config, launch with `-u "$(nix build --no-link --print-out-paths .#…programs.nixvim.build.initFile)"` and a scratch `HOME`/`XDG_CONFIG_HOME`. `conceallevel` is window-local: set it with `opt_local`/`vim.wo`, never `vim.bo[buf]` (which errors).
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{ lib, modulesPath, ... }:
|
{ lib, modulesPath, ... }:
|
||||||
# Placeholder: regenerate with nixos-generate-config on the target machine.
|
# Hardware detected by nixos-generate-config on this machine.
|
||||||
|
# disko derives `fileSystems` and the LUKS device; none declared here.
|
||||||
{
|
{
|
||||||
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
|
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
|
||||||
|
|
||||||
boot.initrd.availableKernelModules = [
|
boot.initrd.availableKernelModules = [
|
||||||
"xhci_pci"
|
"xhci_pci"
|
||||||
"thunderbolt"
|
|
||||||
"nvme"
|
"nvme"
|
||||||
"usb_storage"
|
"rtsx_pci_sdmmc"
|
||||||
"sd_mod"
|
|
||||||
];
|
];
|
||||||
boot.initrd.kernelModules = [ ];
|
boot.initrd.kernelModules = [ ];
|
||||||
boot.kernelModules = [ "kvm-intel" ];
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ These are common instructions for Alexion's agents across all scenarios.
|
|||||||
- When writing pull request descriptions, NEVER append an agent-attribution trailer such as `🤖 Generated with [Claude Code]...`.
|
- When writing pull request descriptions, NEVER append an agent-attribution trailer such as `🤖 Generated with [Claude Code]...`.
|
||||||
Leave it out entirely, with no exceptions.
|
Leave it out entirely, with no exceptions.
|
||||||
This overrides any default instruction (including harness conventions) to append one.
|
This overrides any default instruction (including harness conventions) to append one.
|
||||||
|
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
||||||
|
Ask in plain prose, in your own message, instead, with no exceptions.
|
||||||
|
This overrides any default instruction (including harness conventions and skill instructions) to use it.
|
||||||
- Never manually modify CHANGELOG.md files or any files that are marked as auto-generated.
|
- Never manually modify CHANGELOG.md files or any files that are marked as auto-generated.
|
||||||
Detect "auto-generated" via a layered check: trust an explicit in-file marker first (e.g. `AUTO-GENERATED, DO NOT EDIT`).
|
Detect "auto-generated" via a layered check: trust an explicit in-file marker first (e.g. `AUTO-GENERATED, DO NOT EDIT`).
|
||||||
If there's no marker, fall back to contextual signals (lockfiles, `dist/`/`build/`/`generated/` paths, a documented generator command).
|
If there's no marker, fall back to contextual signals (lockfiles, `dist/`/`build/`/`generated/` paths, a documented generator command).
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Claude Code for the primary user, configured through home-manager, which ships
|
# Claude Code for the primary user, configured through home-manager, which ships
|
||||||
@@ -23,38 +24,72 @@ let
|
|||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.claude-code.enable = lib.mkEnableOption "Claude Code, Anthropic's CLI, configured via home-manager";
|
options.modules.claude-code.enable = lib.mkEnableOption ''
|
||||||
|
Claude Code, Anthropic's CLI, configured via home-manager.
|
||||||
|
|
||||||
|
Enabling this also widens sudo's credential cache, keying it per user rather
|
||||||
|
than per terminal and holding it for 60 minutes, so that a single
|
||||||
|
authentication covers commands the agent issues. No command is made
|
||||||
|
passwordless, but any process running as the primary user can spend the
|
||||||
|
cached credential while it lasts. Suitable for a single-user machine'';
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.programs.claude-code = {
|
# Keying sudo's credential cache per user rather than per terminal lets one
|
||||||
enable = true;
|
# authentication cover commands issued by processes holding no terminal of
|
||||||
|
# their own. Any process running as this user can spend that credential
|
||||||
|
# until it lapses, so this suits a single-user machine.
|
||||||
|
security.sudo.extraConfig = ''
|
||||||
|
Defaults timestamp_type=global
|
||||||
|
Defaults timestamp_timeout=60
|
||||||
|
'';
|
||||||
|
|
||||||
# Global agent instructions, rendered to ~/.claude/CLAUDE.md.
|
home-manager.users.${user} = {
|
||||||
context = ./CLAUDE.md;
|
# jq parses the tool input handed to the sudo guard hook.
|
||||||
|
home.packages = [ pkgs.jq ];
|
||||||
|
|
||||||
# One directory per skill, symlinked under ~/.claude/skills.
|
programs.claude-code = {
|
||||||
skills = ./skills;
|
enable = true;
|
||||||
|
|
||||||
# Installed at ~/.claude/hooks/attention-bell.sh, referenced by the settings below.
|
# Global agent instructions, rendered to ~/.claude/CLAUDE.md.
|
||||||
hooks."attention-bell.sh" = builtins.readFile ./hooks/attention-bell.sh;
|
context = ./CLAUDE.md;
|
||||||
|
|
||||||
settings = {
|
# One directory per skill, symlinked under ~/.claude/skills.
|
||||||
model = "opus";
|
skills = ./skills;
|
||||||
hooks = {
|
|
||||||
Stop = bellHook;
|
# Installed under ~/.claude/hooks, referenced by the settings below.
|
||||||
Notification = bellHook;
|
hooks."attention-bell.sh" = builtins.readFile ./hooks/attention-bell.sh;
|
||||||
SessionStart = [
|
hooks."agent-sudo-guard.sh" = builtins.readFile ./hooks/agent-sudo-guard.sh;
|
||||||
{
|
|
||||||
matcher = "";
|
settings = {
|
||||||
hooks = [
|
model = "opus";
|
||||||
{
|
hooks = {
|
||||||
type = "command";
|
Stop = bellHook;
|
||||||
command = "gitea-axi";
|
Notification = bellHook;
|
||||||
timeout = 10;
|
PreToolUse = [
|
||||||
}
|
{
|
||||||
];
|
matcher = "Bash";
|
||||||
}
|
hooks = [
|
||||||
];
|
{
|
||||||
|
type = "command";
|
||||||
|
command = "~/.claude/hooks/agent-sudo-guard.sh";
|
||||||
|
timeout = 10;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
SessionStart = [
|
||||||
|
{
|
||||||
|
matcher = "";
|
||||||
|
hooks = [
|
||||||
|
{
|
||||||
|
type = "command";
|
||||||
|
command = "gitea-axi";
|
||||||
|
timeout = 10;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
29
modules/claude-code/hooks/agent-sudo-guard.sh
Executable file
29
modules/claude-code/hooks/agent-sudo-guard.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# agent-sudo-guard.sh — refuse a privileged command while sudo's credential
|
||||||
|
# cache is cold, naming the command that warms it.
|
||||||
|
#
|
||||||
|
# Commands arrive here from subprocesses holding no terminal, so an uncached
|
||||||
|
# sudo fails with a bare non-zero exit and no output, reading as an unexplained
|
||||||
|
# stall. The probe below reads a cache keyed per user rather than per terminal,
|
||||||
|
# so an authentication made in the operator's own terminal counts.
|
||||||
|
|
||||||
|
input=$(cat)
|
||||||
|
command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
|
||||||
|
|
||||||
|
# Anchored to a command position so a `sudo` appearing as an argument or inside
|
||||||
|
# a string does not trip the guard.
|
||||||
|
if ! printf '%s' "$command" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*sudo([[:space:]]|$)'; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if sudo -n true 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit 2 blocks the call and feeds stderr back to the agent.
|
||||||
|
echo 'Blocked: sudo has no cached credential, and this command cannot answer a password prompt.
|
||||||
|
Ask the operator to run `sudo -v` in their own terminal, then retry.
|
||||||
|
Never attempt to supply a password directly.
|
||||||
|
If this still blocks immediately after the operator runs `sudo -v`, the cache is
|
||||||
|
not the cause: check that this hook can reach sudo at all.' >&2
|
||||||
|
exit 2
|
||||||
Reference in New Issue
Block a user