Skip to content

refactor: extract all inline home.activation scripts to bash files - #1443

Merged
shunkakinoki merged 17 commits into
mainfrom
refactor/extract-activation-scripts
Apr 12, 2026
Merged

refactor: extract all inline home.activation scripts to bash files#1443
shunkakinoki merged 17 commits into
mainfrom
refactor/extract-activation-scripts

Conversation

@shunkakinoki

@shunkakinoki shunkakinoki commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Extract all inline shell from home.activation blocks in .nix files to external .sh scripts invoked via ${pkgs.bash}/bin/bash
  • 21 new activation .sh files + 3 shared scripts in home-manager/activation/ (ensure-directory, deploy-agenix-secret, import-gpg-key)
  • Boot detection moved from inline Nix conditionals to .sh scripts via SYSTEMCTL_BIN env var
  • Extended check-nix-inline-scripts.sh to lint activation blocks (not just writeScript)
  • 10 new spec files covering all extracted scripts; all 1195 tests pass

Test plan

  • make shell-test - 1195 examples, 0 failures
  • make shell-inline-check - passes
  • nix eval .#darwinConfigurations.aarch64-darwin.system --impure - evaluates successfully
  • CI green on shell-lint, shell-test, shell-inline-check

Summary by cubic

Moved all inline home.activation shell to external .sh files run via ${pkgs.bash}/bin/bash for clearer, testable activation and stronger ShellCheck coverage. Also tightened the inline-script checker, added act, switched select configs to tracked JSON with activation-time injection, and improved the ShellCheck runner and ignores.

  • Refactors

    • Extracted activation logic into scripts: Tailscale service install/dirs, non‑NixOS /bin shell symlinks, k3s config sync, Neovim native builds + pack-lock copy, fnm Node setup, OpenClaw/Paperclip dirs, and app configs (claude, cursor, codex, gemini, serena, omp, git-ai, obsidian).
    • Replaced Nix-generated configs with tracked JSON + activation scripts: git-ai injects git_path via jq/sed; Obsidian uses obsidian.json with home path injected via sed.
    • Added shared helpers: ensure-directory.sh, deploy-agenix-secret.sh, import-gpg-key.sh; moved host-specific steps into scripts (Linux/Darwin backups, Kyber IP forwarding). Boot checks moved into scripts via SYSTEMCTL_BIN; rustup setup moved into install-cargo-globals.sh via RUSTUP_BIN.
    • Quoted all activation script args and paths; Makefile nix-build now depends on nix-trust; added act to packages.
    • Simplified ShellCheck runner to scan tracked .sh files via git ls-files; updated .gitignore to ignore dotagents and dotagents/private-skills.
  • Bug Fixes

    • Extended inline-script checker to flag home.activation blocks unless delegating to ${pkgs.bash}/bin/bash; $DRY_RUN_CMD no longer bypasses the check.
    • Added ShellCheck disables for SC2016/SC2012 and targeted quoting cases in activation scripts; aligned/removed unnecessary disables in specs.

Written for commit 860d305. Summary will update on new commits.

Move all inline shell code from home.activation blocks in .nix files
to external .sh scripts invoked via ${pkgs.bash}/bin/bash. This ensures
consistent bash detection and makes activation logic shellcheck-able.

- 21 new .sh files for activation scripts
- 3 shared scripts in home-manager/activation/ (ensure-directory,
  deploy-agenix-secret, import-gpg-key) used by matic + kyber
- Boot detection moved from inline Nix conditionals to .sh scripts
  via SYSTEMCTL_BIN env var (npm/cargo/uv-globals)
- Rustup setup moved to install-cargo-globals.sh via RUSTUP_BIN
- Updated check-nix-inline-scripts.sh to also lint activation blocks
- 10 new spec files with tests for all extracted scripts
- Updated coverage_spec.sh with all new scripts
@mesa-dot-dev

mesa-dot-dev Bot commented Apr 12, 2026

Copy link
Copy Markdown

You do not have enough credits to review this pull request. Please purchase more credits to continue.

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@shunkakinoki has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 14 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 1 minutes and 14 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5bb3d8bb-f07e-4ce5-bf6b-6208cedee408

📥 Commits

Reviewing files that changed from the base of the PR and between 533f320 and 860d305.

📒 Files selected for processing (6)
  • .gitignore
  • Makefile
  • spec/activate_config_spec.sh
  • spec/activate_kyber_spec.sh
  • spec/activate_neovim_spec.sh
  • spec/check_nix_inline_scripts_spec.sh
📝 Walkthrough

Walkthrough

Refactors many inline Nix activation snippets into standalone Bash scripts and updates Nix modules to delegate activation to those scripts; adds shared activation helpers and extensive shell-spec tests covering creation, copying, permissioning, privilege escalation, and boot-time guards.

Changes

Cohort / File(s) Summary
Config activation scripts
config/claude/activate.sh, config/codex/activate.sh, config/cursor/activate.sh, config/gemini/activate.sh, config/k3s/activate.sh, config/omp/activate.sh, config/serena/activate.sh, config/git-ai/activate.sh, config/obsidian/activate.sh
New executable Bash scripts implementing former inline activation logic: directory creation, conditional copying, permissioning, symlink management, jq/sed JSON injection, and sudo/doas detection where needed.
Config Nix module updates
config/claude/default.nix, config/codex/default.nix, config/cursor/default.nix, config/gemini/default.nix, config/k3s/default.nix, config/omp/default.nix, config/serena/default.nix, config/git-ai/default.nix, config/obsidian/default.nix
Module signatures extended to accept pkgs/lib as required; inline activation bodies removed and replaced with ${pkgs.bash}/bin/bash "${./activate.sh}" invocations (arguments quoted/passed as needed).
Home-manager shared activation utilities
home-manager/activation/ensure-directory.sh, home-manager/activation/deploy-agenix-secret.sh, home-manager/activation/import-gpg-key.sh
Added reusable scripts for directory creation+chmod, conditional agenix secret decryption/deployment, and conditional GPG secret import with temp-file cleanup.
Home-manager module activation scripts
home-manager/modules/bin-shells/activate.sh, home-manager/modules/openclaw/activate.sh, home-manager/modules/paperclip/activate.sh, home-manager/modules/tailscale/activate-create-dirs.sh, home-manager/modules/tailscale/activate-install-service.sh
New scripts handling /bin symlinks, OpenClaw/Paperclip directory setup, tailscale dirs and service installation with privilege escalation and idempotent update checks.
Home-manager module Nix updates
home-manager/modules/bin-shells/default.nix, home-manager/modules/openclaw/default.nix, home-manager/modules/paperclip/default.nix, home-manager/modules/cargo-globals/default.nix, home-manager/modules/npm-globals/default.nix, home-manager/modules/uv-globals/default.nix, home-manager/modules/tailscale/default.nix
Replaced inline activation scripts with delegated script calls; moved boot/start guards into scripts and exported SYSTEMCTL_BIN/RUSTUP_BIN where needed.
Home-manager program activation scripts
home-manager/programs/fnm/activate.sh, home-manager/programs/neovim/activate-copy-pack-lock.sh, home-manager/programs/neovim/activate-build-plugins.sh
New scripts for fnm version install/defaulting and creating stable node symlink; Neovim pack-lock copying and native plugin build/download logic.
Home-manager program Nix updates
home-manager/programs/fnm/default.nix, home-manager/programs/neovim/default.nix
Delegated fnm and Neovim activation logic to external scripts and updated activation invocations to pass quoted arguments.
Host activation scripts
hosts/darwin/activate-remove-backups.sh, hosts/linux/activate-backup-files.sh, named-hosts/kyber/activate-backup-files.sh, named-hosts/kyber/activate-ip-forwarding.sh
New host-targeted scripts for backup cleanup, pre-link backups, and enabling IP forwarding (with privilege escalation and persistence).
Host Nix updates
hosts/darwin/default.nix, hosts/linux/default.nix, named-hosts/kyber/default.nix, named-hosts/matic/default.nix
Replaced inline mv/find/sysctl/sudoers logic with delegated script invocations and wired shared helpers (e.g., ensure-directory, deploy/import scripts).
Globals installers
home-manager/modules/cargo-globals/install-cargo-globals.sh, home-manager/modules/npm-globals/install-npm-globals.sh, home-manager/modules/uv-globals/install-uv-globals.sh
Added early-boot guards using SYSTEMCTL_BIN is-system-running and (cargo) pre-install rustup toolchain steps; installer logic moved/centralized into scripts.
Script validation & tests
scripts/check-nix-inline-scripts.sh, many spec/*.sh files (new/updated)
Expanded inline-script detection to include home.activation.* blocks and added/updated numerous shell-spec tests covering shebang, strict mode, directory/file ops, permissioning, privilege escalation, boot guards, and wrapper quoting.
Miscellaneous
Makefile, home-manager/packages/default.nix, config/openclaw/hydrate.sh, home-manager/modules/local-binaries/default.nix
Minor updates: Makefile target deps, added act to packages, small hydrate.sh mkdir fix, and quoting of script path in activation invocation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

🐰
I hop and tidy scripts so neat,
From Nixy nests to bashy street.
Tests bloom like clover, actions sure,
Hooks now simple, tidy, and pure.
Hooray — deployment's snug and sweet!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: extract all inline home.activation scripts to bash files' clearly and accurately summarizes the main change: extracting inline shell from home.activation Nix blocks into external bash scripts.
Description check ✅ Passed The pull request description clearly explains the purpose: extracting inline home.activation shell code into external scripts, adding test coverage, and refactoring app configs to tracked JSON with activation-time injection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/extract-activation-scripts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mesa-dot-dev

mesa-dot-dev Bot commented Apr 12, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Refactored the dotfiles codebase by extracting all inline shell scripts from home.activation blocks in Nix files into external .sh scripts. This improves code clarity, testability, and enables better linting for activation logic, moving boot detection into these scripts and standardizing configuration management.

What changed?

Activation Logic Extraction

  • Shared Utilities: Added home-manager/activation/deploy-agenix-secret.sh, ensure-directory.sh, and import-gpg-key.sh for common activation tasks.
  • Tailscale: Moved service installation and directory creation logic to home-manager/modules/tailscale/activate-install-service.sh and other external scripts, refactoring home-manager/modules/tailscale/default.nix.
  • Neovim: Extracted plugin build and nvim-pack-lock.json copy logic into home-manager/programs/neovim/activate-build-plugins.sh and activate-copy-pack-lock.sh, refactoring home-manager/programs/neovim/default.nix.
  • Node.js (fnm): Moved fnm setup, Node.js installation, and symlink creation to home-manager/programs/fnm/activate.sh, refactoring home-manager/programs/fnm/default.nix.
  • Configuration Activation: Refactored config/claude/default.nix, config/codex/default.nix, config/cursor/default.nix, config/gemini/default.nix, config/k3s/default.nix, config/omp/default.nix, config/serena/default.nix to use dedicated activate.sh scripts for managing their respective configurations instead of inline shell.
  • Binary Shells: Externalized logic for creating /bin shell symlinks into home-manager/modules/bin-shells/activate.sh, refactoring home-manager/modules/bin-shells/default.nix.
  • Global Packages (Cargo, npm, uv): Moved boot-time checks and installation logic from home.activation blocks in home-manager/modules/cargo-globals/default.nix, home-manager/modules/npm-globals/default.nix, and home-manager/modules/uv-globals/default.nix to their respective external install-*-globals.sh scripts.
  • Directory Setup: Refactored home-manager/modules/openclaw/default.nix and home-manager/modules/paperclip/default.nix to use external activate.sh scripts for directory creation and permissions.
  • Host-Specific Logic: Extracted backup and cleanup procedures for Darwin and Linux hosts into hosts/darwin/activate-remove-backups.sh and hosts/linux/activate-backup-files.sh, respectively, and moved various host-specific activation steps for Kyber and Matic into external scripts, refactoring named-hosts/kyber/default.nix and named-hosts/matic/default.nix.

Configuration Management

  • Obsidian: Changed configuration from pkgs.writeText to a dynamic activate.sh script (config/obsidian/activate.sh) that templates config/obsidian/obsidian.json with the home directory via sed, refactoring config/obsidian/default.nix.
  • Git-AI: Refactored config/git-ai/default.nix to use config/git-ai/activate.sh for copying and injecting the Git path into config.json.

Linting and Testing Enhancements

  • Inline Script Checker: Extended scripts/check-nix-inline-scripts.sh to prevent inline shell in home.activation blocks unless delegating to external bash scripts, and added new test cases in spec/check_nix_inline_scripts_spec.sh.
  • New Test Specifications: Added comprehensive shellspec tests for extracted scripts including spec/activate_bin_shells_spec.sh, spec/activate_fnm_spec.sh, spec/activate_tailscale_spec.sh, spec/activation_shared_spec.sh, spec/activate_config_spec.sh, spec/activate_kyber_spec.sh, spec/activate_neovim_spec.sh, and spec/activate_paperclip_openclaw_spec.sh.
  • Test Coverage: Updated spec/coverage_spec.sh.

Miscellaneous

  • Makefile: Added nix-trust as a prerequisite to the nix-build target in Makefile.
  • Packages: Added the act package to home-manager/packages/default.nix.
  • Path Quoting: Ensured correct quoting of paths in home-manager/modules/local-binaries/default.nix.

Description generated by Mesa. Update settings

@mesa-dot-dev

mesa-dot-dev Bot commented Apr 12, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Extracted all inline home.activation shell logic into external, testable bash scripts to improve clarity, maintainability, and ShellCheck coverage, while also enhancing the inline-script checker.

What changed?

  • home-manager/activation/deploy-agenix-secret.sh: New script to decrypt and deploy agenix secrets securely.
  • home-manager/activation/ensure-directory.sh: New utility script to create directories with specified permissions.
  • home-manager/activation/import-gpg-key.sh: New script to automate GPG secret key import from agenix-encrypted files.
  • home-manager/modules/tailscale/activate-install-service.sh: New script to automate Tailscale system service installation and sudoers configuration.
  • home-manager/programs/neovim/activate-build-plugins.sh: New script to automate building native Neovim plugins and downloading prebuilt binaries.
  • scripts/check-nix-inline-scripts.sh: Enhanced to prevent inline shell in home.activation blocks unless delegating to external bash scripts, with clearer error messages.
  • home-manager/programs/fnm/activate.sh: New script to manage Node.js versions with fnm, including installation, default setting, and symlink creation.
  • spec/activate_bin_shells_spec.sh: New tests for home-manager/modules/bin-shells/activate.sh covering symlink creation and bash best practices.
  • spec/activate_fnm_spec.sh: New tests for home-manager/programs/fnm/activate.sh verifying Node.js installation, version setting, and symlink creation.
  • spec/activate_tailscale_spec.sh: New tests for Tailscale activation scripts, verifying directory creation, permissions, and service installation.
  • spec/activation_shared_spec.sh: New shared tests for ensure-directory.sh, deploy-agenix-secret.sh, and import-gpg-key.sh, verifying core functionalities and error handling.
  • config/obsidian/default.nix: Refactored Obsidian configuration to use an activate.sh script for dynamic templating of obsidian.json with the home directory via sed.
  • config/git-ai/activate.sh: New script to set up git-ai configuration, injecting the Git path into config.json.
  • config/git-ai/default.nix: Refactored git-ai configuration to use activate.sh for config.json setup, passing config and git path as arguments.
  • config/obsidian/activate.sh: New script to process obsidian.json by replacing __HOME_DIR__ with the actual home directory.
  • spec/activate_config_spec.sh: New tests for activating various configurations (codex, claude, cursor, omp, gemini, serena, git-ai, obsidian), verifying file operations and placeholder substitutions.
  • spec/coverage_spec.sh: Updated test suite for code coverage.
  • config/obsidian/obsidian.json: Updated to include a new "wiki" vault configuration and a minor syntax fix.
  • Makefile: Updated nix-build target to include nix-trust as a prerequisite.
  • config/claude/default.nix: Refactored Claude settings.json activation to use an external activate.sh script via pkgs.bash.
  • config/codex/default.nix: Refactored codex activation to use an external activate.sh script via pkgs.bash for config.toml and hooks.json.
  • config/cursor/default.nix: Refactored cursorHooks configuration to use an activate.sh script via pkgs.bash.
  • config/gemini/default.nix: Refactored gemini settings.json copying to use an external activate.sh script via pkgs.bash.
  • config/k3s/default.nix: Refactored k3s configuration activation to replace inline bash with a call to an external activate.sh script.
  • config/omp/default.nix: Refactored omp configuration setup to delegate to an external activate.sh script via pkgs.bash.
  • config/openclaw/default.nix: Refined home.activation.hydrateOpenclawConfig by removing explicit directory creation and improving script execution robustness.
  • config/serena/default.nix: Refactored Serena serena_config.yml copying to use an external activate.sh script.
  • home-manager/modules/bin-shells/default.nix: Refactored /bin shell symlink creation to use an external activate.sh script.
  • home-manager/modules/cargo-globals/default.nix: Refactored installCargoGlobals activation script by externalizing boot checks and Rust toolchain installation logic to install-cargo-globals.sh.
  • home-manager/modules/local-binaries/default.nix: Updated home.activation.symlinkLocalBinaries to correctly quote the sync-local-binaries.sh path.
  • home-manager/modules/npm-globals/default.nix: Refactored boot-time check for npm global package installation by moving systemctl logic into install-npm-globals.sh.
  • home-manager/modules/openclaw/default.nix: Refactored openclaw directory setup and permission logic into a new external activate.sh script.
  • home-manager/modules/paperclip/default.nix: Refactored Paperclip directory setup and permission configuration to an external ./activate.sh bash script.
  • home-manager/modules/tailscale/default.nix: Refactored Tailscale module by moving complex shell scripts for directory creation and service installation into external bash scripts.
  • home-manager/modules/uv-globals/default.nix: Refactored boot-time check for uv-globals installation by moving systemctl logic into install-uv-globals.sh.
  • home-manager/packages/default.nix: Added the 'act' package to the list of packages.
  • home-manager/programs/fnm/default.nix: Refactored fnm setup activation script by moving all inline shell logic to an external activate.sh bash script.
  • home-manager/programs/neovim/default.nix: Refactored Neovim configuration by extracting inline shell commands for nvim-pack-lock.json copying and native plugin builds into dedicated bash scripts (activate-copy-pack-lock.sh and activate-build-plugins.sh).
  • hosts/darwin/default.nix: Replaced an inline find command for deleting *.hm-backup* files with the execution of a new external bash script, ./activate-remove-backups.sh.
  • hosts/linux/default.nix: Refactored the preActivation hook to delegate configuration file backup and cleanup to an external activate-backup-files.sh script.
  • named-hosts/kyber/default.nix: Refactored home.activation scripts by replacing inline bash with calls to external dedicated bash scripts for tasks like backing up Home Manager files, configuring SSH/Agenix, deploying secrets, importing GPG keys, and enabling Tailscale IP forwarding.
  • named-hosts/matic/default.nix: Refactored home.activation hooks by externalizing inline shell commands for tasks such as ensuring SSH/agenix directories, deploying agenix secrets, and importing GPG keys, into dedicated bash scripts.
  • spec/activate_kyber_spec.sh: New tests for activate-backup-files.sh, activate-ip-forwarding.sh, and the default.nix configuration within named-hosts/kyber.
  • spec/activate_neovim_spec.sh: New tests for Neovim activation scripts, ensuring correct configuration setup, file copying, and plugin builds/downloads.
  • spec/activate_paperclip_openclaw_spec.sh: New integration tests for home-manager modules paperclip and openclaw, verifying script behavior and directory permissions.
  • spec/check_nix_inline_scripts_spec.sh: Significantly enhanced test suite for scripts/check-nix-inline-scripts.sh, verifying its ability to correctly handle delegated bash activation.

Description generated by Mesa. Update settings

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="home-manager/activation/deploy-agenix-secret.sh">

<violation number="1" location="home-manager/activation/deploy-agenix-secret.sh:10">
P2: Re-add quotes around `DEST` in the file test to avoid test parse errors when the argument is empty.</violation>

<violation number="2" location="home-manager/activation/deploy-agenix-secret.sh:15">
P2: Quote `SECRET_FILE` in the file-existence check to keep behavior safe for empty input values.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

fi

echo "Deploying secret from agenix..."
if [[ ! -f $SECRET_FILE ]]; then

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Quote SECRET_FILE in the file-existence check to keep behavior safe for empty input values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/activation/deploy-agenix-secret.sh, line 15:

<comment>Quote `SECRET_FILE` in the file-existence check to keep behavior safe for empty input values.</comment>

<file context>
@@ -7,12 +7,12 @@ SECRET_FILE="$2"
 
 echo "Deploying secret from agenix..."
-if [[ ! -f "$SECRET_FILE" ]]; then
+if [[ ! -f $SECRET_FILE ]]; then
   echo "Warning: Secret file not found at $SECRET_FILE" >&2
   exit 0
</file context>
Suggested change
if [[ ! -f $SECRET_FILE ]]; then
if [[ ! -f "$SECRET_FILE" ]]; then
Fix with Cubic

IDENTITY_KEY="$3"
RAGE_BIN="$4"

if [[ -f $DEST ]]; then

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Re-add quotes around DEST in the file test to avoid test parse errors when the argument is empty.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/activation/deploy-agenix-secret.sh, line 10:

<comment>Re-add quotes around `DEST` in the file test to avoid test parse errors when the argument is empty.</comment>

<file context>
@@ -7,12 +7,12 @@ SECRET_FILE="$2"
 RAGE_BIN="$4"
 
-if [[ -f "$DEST" ]]; then
+if [[ -f $DEST ]]; then
   exit 0
 fi
</file context>
Suggested change
if [[ -f $DEST ]]; then
if [[ -f "$DEST" ]]; then
Fix with Cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 62 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="home-manager/programs/fnm/default.nix">

<violation number="1" location="home-manager/programs/fnm/default.nix:28">
P2: Quote/escape activation script arguments to avoid argv splitting when `homeDir` contains shell-significant characters.</violation>
</file>

<file name="home-manager/modules/cargo-globals/install-cargo-globals.sh">

<violation number="1" location="home-manager/modules/cargo-globals/install-cargo-globals.sh:13">
P2: Avoid mutating the user’s global rustup default in activation. Use a script-local stable toolchain selection instead.</violation>
</file>

<file name="home-manager/activation/import-gpg-key.sh">

<violation number="1" location="home-manager/activation/import-gpg-key.sh:25">
P1: Ensure the decrypted temp key file is removed even when GPG import fails; with `set -e`, the current order can leave sensitive key material on disk.</violation>
</file>

<file name="scripts/check-nix-inline-scripts.sh">

<violation number="1" location="scripts/check-nix-inline-scripts.sh:76">
P2: The activation lint only reports violations when no `bin/bash` call exists, so blocks with a bash delegation plus extra inline shell commands incorrectly pass.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


echo "Importing GPG key from agenix..."
if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
"$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Ensure the decrypted temp key file is removed even when GPG import fails; with set -e, the current order can leave sensitive key material on disk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/activation/import-gpg-key.sh, line 25:

<comment>Ensure the decrypted temp key file is removed even when GPG import fails; with `set -e`, the current order can leave sensitive key material on disk.</comment>

<file context>
@@ -0,0 +1,28 @@
+
+echo "Importing GPG key from agenix..."
+if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
+  "$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null
+  rm -f "$GPG_TEMP_FILE"
+  echo "GPG key imported successfully"
</file context>
Fix with Cubic

Comment thread home-manager/programs/fnm/default.nix Outdated
Comment on lines +13 to +14
"$RUSTUP_BIN" toolchain install stable --no-self-update || true
"$RUSTUP_BIN" default stable || true

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid mutating the user’s global rustup default in activation. Use a script-local stable toolchain selection instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/modules/cargo-globals/install-cargo-globals.sh, line 13:

<comment>Avoid mutating the user’s global rustup default in activation. Use a script-local stable toolchain selection instead.</comment>

<file context>
@@ -2,6 +2,18 @@
+
+# Ensure stable toolchain is installed (RUSTUP_BIN set by nix activation)
+if [ -n "${RUSTUP_BIN:-}" ]; then
+  "$RUSTUP_BIN" toolchain install stable --no-self-update || true
+  "$RUSTUP_BIN" default stable || true
+fi
</file context>
Suggested change
"$RUSTUP_BIN" toolchain install stable --no-self-update || true
"$RUSTUP_BIN" default stable || true
"$RUSTUP_BIN" toolchain install stable --no-self-update || true
export RUSTUP_TOOLCHAIN=stable
Fix with Cubic

Comment thread scripts/check-nix-inline-scripts.sh Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Nix configuration by moving inline shell scripts from home.activation blocks into external bash scripts across numerous modules, including Tailscale, Neovim, and various AI tool configurations. It introduces shared activation helpers for directory management and secret deployment, while updating the repository linter to enforce this separation. Feedback highlights several critical improvements: implementing trap for secure GPG key cleanup, validating sudoers syntax with visudo before deployment, correcting a version-matching bug in the fnm script, and addressing platform-specific issues in the Neovim plugin build process. Additionally, it is recommended to refactor duplicated sudo detection logic and tighten the linter's validation of inline shell code.

Comment on lines +23 to +28
echo "Importing GPG key from agenix..."
if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
"$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null
rm -f "$GPG_TEMP_FILE"
echo "GPG key imported successfully"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The decrypted GPG private key is written to a temporary file on disk. If the script is interrupted or if the gpg --import command fails, this sensitive file may not be deleted, potentially exposing the private key. It is recommended to use a trap to ensure cleanup.

Suggested change
echo "Importing GPG key from agenix..."
if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
"$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null
rm -f "$GPG_TEMP_FILE"
echo "GPG key imported successfully"
fi
echo "Importing GPG key from agenix..."
trap 'rm -f "$GPG_TEMP_FILE"' EXIT
if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
"$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null
echo "GPG key imported successfully"
fi

Comment on lines +36 to +38
run_root_cmd ln -sf "$BASH_PATH" /bin/bash
run_root_cmd ln -sf "$FISH_PATH" /bin/fish
run_root_cmd ln -sf "$ZSH_PATH" /bin/zsh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Overwriting /bin/bash with a symlink to the Nix store on a non-NixOS system is highly intrusive and risky. If the Nix store path becomes invalid or inaccessible, the system's primary shell will be broken, potentially preventing logins or breaking system scripts. It is generally safer to manage user shells via chsh or to provide the Nix-managed shells in the user's PATH rather than modifying system-wide paths in /bin.

Comment on lines +53 to +57
if ! cmp -s "$TEMP_SUDOERS" "$SUDOERS_FILE" 2>/dev/null; then
run_root_cmd cp "$TEMP_SUDOERS" "$SUDOERS_FILE"
run_root_cmd chmod 0440 "$SUDOERS_FILE"
echo "Sudo PATH configured. You can now use: sudo tailscale login"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The script overwrites the sudoers configuration without validating the syntax of the new file. A syntax error in /etc/sudoers.d/ can break sudo access for the entire system, requiring manual recovery. Use visudo -cf to validate the temporary file before copying it to the destination.

Suggested change
if ! cmp -s "$TEMP_SUDOERS" "$SUDOERS_FILE" 2>/dev/null; then
run_root_cmd cp "$TEMP_SUDOERS" "$SUDOERS_FILE"
run_root_cmd chmod 0440 "$SUDOERS_FILE"
echo "Sudo PATH configured. You can now use: sudo tailscale login"
fi
if ! cmp -s "$TEMP_SUDOERS" "$SUDOERS_FILE" 2>/dev/null; then
if ! visudo -cf "$TEMP_SUDOERS" >/dev/null 2>&1; then
echo "Error: Invalid sudoers syntax generated" >&2
rm -f "$TEMP_SUDOERS"
exit 1
fi
run_root_cmd cp "$TEMP_SUDOERS" "$SUDOERS_FILE"
run_root_cmd chmod 0440 "$SUDOERS_FILE"
echo "Sudo PATH configured. You can now use: sudo tailscale login"
fi

Comment on lines +34 to +51
if [ -n "$fff_version" ]; then
_arch=$(uname -m)
_ldd=$(ldd --version 2>&1 || echo "")
if echo "$_ldd" | grep -q musl; then
_triple="${_arch}-unknown-linux-musl"
else
_triple="${_arch}-unknown-linux-gnu"
fi
mkdir -p "$fff_dir/target"
echo "Fetching https://github.com/dmtrKovalenko/fff.nvim/releases/download/$fff_version/${_triple}.${LIB_EXT}"
curl --fail --location --silent --show-error \
-o "$fff_binary" \
"https://github.com/dmtrKovalenko/fff.nvim/releases/download/$fff_version/${_triple}.${LIB_EXT}" &&
echo "fff.nvim binary downloaded successfully" ||
echo "fff.nvim binary download failed (will fall back to build on first use)"
else
echo "fff.nvim: could not determine version, skipping download"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The script assumes a Linux environment when constructing the download URL for the fff.nvim binary. On macOS, ldd is not available, and the resulting _triple (e.g., arm64-unknown-linux-gnu) will likely lead to a 404 error or an incompatible binary download. Additionally, consider adding a network connectivity check before attempting the download to avoid failures in offline environments.

    if [ -n "$fff_version" ]; then
      if ! timeout 3 bash -c 'exec 3<>/dev/tcp/1.1.1.1/53' 2>/dev/null; then
        echo "fff.nvim: network unavailable, skipping download"
      else
        _arch=$(uname -m)
        if [ "$(uname)" = "Darwin" ]; then
          _triple="${_arch}-apple-darwin"
        else
          _ldd=$(ldd --version 2>&1 || echo "")
          if echo "$_ldd" | grep -q musl; then
            _triple="${_arch}-unknown-linux-musl"
          else
            _triple="${_arch}-unknown-linux-gnu"
          fi
        fi
        mkdir -p "$fff_dir/target"
        echo "Fetching https://github.com/dmtrKovalenko/fff.nvim/releases/download/$fff_version/${_triple}.${LIB_EXT}"
        curl --fail --location --silent --show-error \
          -o "$fff_binary" \
          "https://github.com/dmtrKovalenko/fff.nvim/releases/download/$fff_version/${_triple}.${LIB_EXT}" \
          && echo "fff.nvim binary downloaded successfully" \
          || echo "fff.nvim binary download failed (will fall back to build on first use)"
      fi
    else
      echo "fff.nvim: could not determine version, skipping download"
    fi

Comment thread config/k3s/activate.sh
Comment on lines +9 to +16
SUDO_CMD=""
if command -v sudo >/dev/null 2>&1; then
SUDO_CMD="sudo"
elif [ -x /run/wrappers/bin/sudo ]; then
SUDO_CMD="/run/wrappers/bin/sudo"
elif [ -x /usr/bin/sudo ]; then
SUDO_CMD="/usr/bin/sudo"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The sudo detection logic is duplicated across multiple activation scripts (k3s, bin-shells, tailscale, ip-forwarding). This logic should be refactored into a shared helper script in home-manager/activation/ to improve maintainability and ensure consistent behavior (e.g., consistent support for doas and error handling).


# Install each version if not already installed
for version in "$DEFAULT_VERSION" "$@"; do
if ! ls -d "$FNM_DIR/node-versions/v${version}"* >/dev/null 2>&1; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ls -d check for existing Node.js versions is too broad. For example, if version is "2", the pattern v2* will match an existing v22 installation, causing the script to incorrectly skip the installation of version 2. Use a more precise pattern or fnm list with a strict regex to verify the installed version.

Suggested change
if ! ls -d "$FNM_DIR/node-versions/v${version}"* >/dev/null 2>&1; then
if ! "$FNM_BIN" list 2>/dev/null | grep -qE "v${version}(\s|$|\.)"; then

Comment thread scripts/check-nix-inline-scripts.sh Outdated
# Anything else is a violation
has_violation = 1
}
if (has_violation && !has_bash_call) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The linter logic if (has_violation && !has_bash_call) is too permissive. It allows inline shell code within a home.activation block as long as there is at least one call to bin/bash. This defeats the purpose of the check, which is to ensure that all logic is moved to external scripts. The check should flag any block where has_violation is true.

Suggested change
if (has_violation && !has_bash_call) {
if (has_violation) {

Remove false-negative where $DRY_RUN_CMD lines were whitelisted
unconditionally. Now any non-export, non-bash-delegation line is
flagged, even with $DRY_RUN_CMD prefix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (22)
scripts/check-nix-inline-scripts.sh-67-75 (1)

67-75: ⚠️ Potential issue | 🟠 Major

Allowlist is too broad and can let inline commands pass undetected.

Lines 71–75 allow ${...}, quoted strings, and bare identifiers unconditionally. That permits inline command lines (for example, ${pkgs.coreutils}/bin/mkdir ...) unless they fail some other check. Since has_bash_call is never enforced, blocks can bypass the delegation policy.

Suggested tightening
-        has_bash_call = 0
+        has_bash_call = 0
+        in_bash_continuation = 0
         for (i in lines) {
           line = lines[i]
           gsub(/^[[:space:]]+/, "", line)
           if (line == "") continue
           if (line ~ /^export /) continue
           if (line ~ /^\$\{lib\.optionalString/) continue
-          if (line ~ /bin\/bash/) { has_bash_call = 1; continue }
-          if (line ~ /\\$/) continue
-          if (line ~ /^\$\{/) continue
-          if (line ~ /^".*"$/) continue
-          if (line ~ /^[A-Za-z0-9_-]+$/) continue
+          if (line ~ /^\$DRY_RUN_CMD[[:space:]]+\$\{pkgs\.bash\}\/bin\/bash[[:space:]]+\$\{\.\/[^}]+\}/) {
+            has_bash_call = 1
+            in_bash_continuation = (line ~ /\\$/)
+            continue
+          }
+          if (in_bash_continuation && (line ~ /^\$\{/ || line ~ /^".*"$/ || line ~ /^[A-Za-z0-9_-]+$/)) {
+            in_bash_continuation = (line ~ /\\$/)
+            continue
+          }
           has_violation = 1
         }
+        if (!has_bash_call) {
+          has_violation = 1
+        }
         if (has_violation) {
           printf "%s: inline shell in home.activation block\n", FILENAME
         }

Also applies to: 79-81

scripts/check-nix-inline-scripts.sh-49-49 (1)

49-49: ⚠️ Potential issue | 🟠 Major

Activation block end matcher misses valid '' closers.

Line 49 only accepts '';, but home.activation blocks wrapped in expressions (e.g., mkIf (...)) commonly close with bare '' and put ; later. This can skip/merge blocks and miss violations.

Suggested fix
-    body_started && /^[[:space:]]*''';/ {
+    body_started && /^[[:space:]]*''([[:space:]]*;)?[[:space:]]*$/ {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-nix-inline-scripts.sh` at line 49, The activation-block end
matcher in the body_started conditional only matches closing sequences that
include a trailing semicolon (`'';`) which misses cases where activation blocks
end with a bare `''` and the `;` appears later (e.g., inside `mkIf` wrappers);
update the pattern used in the `body_started && /^[[:space:]]*'"''"';/` test to
also accept a bare `''` closer (and optional following `;`) so the
`body_started` branch correctly detects the end of `home.activation` blocks;
locate the `body_started` conditional and its regex in
scripts/check-nix-inline-scripts.sh and broaden the regex to match `''` with or
without an immediate semicolon.
home-manager/modules/cargo-globals/install-cargo-globals.sh-11-15 (1)

11-15: ⚠️ Potential issue | 🟠 Major

Move rustup setup behind connectivity check and add timeouts.

Lines 11-15 run network-sensitive rustup operations before the offline guard (line 18) and without timeout, which can stall activation on flaky/offline systems.

💡 Suggested patch
-# Ensure stable toolchain is installed (RUSTUP_BIN set by nix activation)
-if [ -n "${RUSTUP_BIN:-}" ]; then
-  "$RUSTUP_BIN" toolchain install stable --no-self-update || true
-  "$RUSTUP_BIN" default stable || true
-fi
-
 # Skip if offline
 if ! timeout 3 bash -c 'exec 3<>/dev/tcp/1.1.1.1/53' 2>/dev/null; then
   echo "Network unavailable, skipping cargo globals install"
   exit 0
 fi
+
+# Ensure stable toolchain is installed (RUSTUP_BIN set by nix activation)
+if [ -n "${RUSTUP_BIN:-}" ]; then
+  timeout 120 "$RUSTUP_BIN" toolchain install stable --no-self-update || true
+  timeout 30 "$RUSTUP_BIN" default stable || true
+fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/modules/cargo-globals/install-cargo-globals.sh` around lines 11
- 15, The rustup network operations (invocations of "$RUSTUP_BIN" toolchain
install/default) must be moved to run after the existing offline guard check and
protected with timeouts so activation won’t hang on flaky or offline systems;
relocate the whole if [ -n "${RUSTUP_BIN:-}" ]; then ... fi block to after the
offline connectivity check, and wrap the rustup calls with a timeout (e.g.,
timeout 10s) and preserve the || true fallback so failures don’t break
activation.
home-manager/modules/cargo-globals/default.nix-28-29 (1)

28-29: ⚠️ Potential issue | 🟠 Major

Propagate the new script env vars to the user service too.

These exports only affect the Home Manager activation path. The Linux systemd.user.services.install-cargo-globals unit at Lines 42-51 runs the same install-cargo-globals.sh, but without RUSTUP_BIN that path skips rustup toolchain install stable / rustup default stable, so login-time behavior no longer matches activation.

🔧 Proposed fix
       Service = {
         Type = "simple";
         Environment = [
           "PATH=${pkgs.rustup}/bin:${pkgs.cargo}/bin:${pkgs.rustc}/bin:${pkgs.dasel}/bin:${pkgs.jq}/bin:${pkgs.gcc}/bin:${pkgs.pkg-config}/bin:${pkgs.perl}/bin:${pkgs.coreutils}/bin:${pkgs.bash}/bin"
           "CARGO_HOME=%h/.cargo"
           "HOME=%h"
+          "SYSTEMCTL_BIN=${pkgs.systemd}/bin/systemctl"
+          "RUSTUP_BIN=${pkgs.rustup}/bin/rustup"
           "PKG_CONFIG_PATH=${pkgs.openssl.dev}/lib/pkgconfig${libiconvPkgConfigPath}"
           "OPENSSL_DIR=${pkgs.openssl.dev}"
           "OPENSSL_LIB_DIR=${pkgs.openssl.out}/lib"
           "OPENSSL_INCLUDE_DIR=${pkgs.openssl.dev}/include"
         ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/modules/cargo-globals/default.nix` around lines 28 - 29, The
activation exports (RUSTUP_BIN and conditional SYSTEMCTL_BIN) are not propagated
into the systemd.user.services.install-cargo-globals unit, so the user service
runs install-cargo-globals.sh without RUSTUP_BIN; update the service definition
(install-cargo-globals) to export the same environment into the unit—e.g., add
Environment=RUSTUP_BIN=${pkgs.rustup}/bin/rustup and, when !isDarwin,
Environment=SYSTEMCTL_BIN=${pkgs.systemd}/bin/systemctl (or point
EnvironmentFile to the same generated env file) so install-cargo-globals.sh sees
the same variables during login as during activation.
config/k3s/activate.sh-18-23 (1)

18-23: ⚠️ Potential issue | 🟠 Major

Handle root-without-sudo and make copy non-interactive.

Current logic skips installation whenever sudo is missing, even if the script runs as root. Also, use cp -f to comply with non-interactive file-operation rules.

Proposed fix
-if [ -n "$SUDO_CMD" ]; then
-  $SUDO_CMD mkdir -p /etc/rancher/k3s
-  $SUDO_CMD cp "$HOME/.config/k3s/config.yaml" /etc/rancher/k3s/config.yaml
+if [ -n "$SUDO_CMD" ]; then
+  "$SUDO_CMD" mkdir -p /etc/rancher/k3s
+  "$SUDO_CMD" cp -f "$HOME/.config/k3s/config.yaml" /etc/rancher/k3s/config.yaml
+elif [ "$(id -u)" -eq 0 ]; then
+  mkdir -p /etc/rancher/k3s
+  cp -f "$HOME/.config/k3s/config.yaml" /etc/rancher/k3s/config.yaml
 else
   echo "Warning: sudo not found, skipping k3s config installation" >&2
 fi

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/k3s/activate.sh` around lines 18 - 23, The script currently only
attempts installation when SUDO_CMD is set; change the logic to: if SUDO_CMD is
available use it, else if the script is running as root (UID=0) run the commands
without sudo, otherwise print the warning; also make the file copy
non-interactive by using cp -f and ensure mkdir uses -p as before; update
references to SUDO_CMD and the commands that create /etc/rancher/k3s and copy
"$HOME/.config/k3s/config.yaml" so they run under sudo when present or directly
when UID=0.
named-hosts/matic/default.nix-520-523 (1)

520-523: ⚠️ Potential issue | 🟠 Major

Potential secret path mismatch for GitHub SSH key deployment.

Line 522 uses id_ed25519.age, but this host’s agenix mapping targets "keys/id_github.age" for ~/.ssh/id_ed25519_github (Line 496-500), and named-hosts/kyber/default.nix uses id_github.age for the same deploy flow. This looks like a likely wrong secret input.

Likely fix
-              ${builtins.toString ../galactica/keys/id_ed25519.age} \
+              ${builtins.toString ../galactica/keys/id_github.age} \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/matic/default.nix` around lines 520 - 523, The deploy call to
deploy-agenix-secret.sh is passing ../galactica/keys/id_ed25519.age but this
host's agenix mapping and the kyber host expect keys named id_github.age for
~/.ssh/id_ed25519_github; update the secret argument passed to
deploy-agenix-secret.sh (the third positional arg in the invocation using
DRY_RUN_CMD and ${pkgs.bash}/bin/bash) to reference the consistent secret name
(keys/id_github.age) so the agenix mapping for id_ed25519_github matches across
hosts.
home-manager/activation/deploy-agenix-secret.sh-20-25 (1)

20-25: ⚠️ Potential issue | 🟠 Major

Harden secret file creation permissions during decrypt.

At Line 20, rage writes directly to "$DEST" before Line 21 applies chmod 0600. File mode is umask-dependent until chmod runs, which can briefly expose secret contents.

Suggested hardening
 set -euo pipefail
+umask 077
 DEST="$1"
 SECRET_FILE="$2"
 IDENTITY_KEY="$3"
 RAGE_BIN="$4"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/activation/deploy-agenix-secret.sh` around lines 20 - 25, The
script currently lets rage write directly to "$DEST" (subject to the current
umask) before chmod 0600 is applied, risking brief exposure; change the flow to
decrypt into a securely-created temporary file in the same directory (create
with mktemp or set umask 077 and create temp with restricted perms), ensure the
temp is chmod 0600 before invoking RAGE_BIN -o "$TMP", on success atomically
move the temp into "$DEST" (mv -f) and remove the temp on failure; reference the
variables RAGE_BIN, IDENTITY_KEY, SECRET_FILE and DEST and ensure cleanup/exit
codes are preserved.
home-manager/activation/import-gpg-key.sh-11-27 (1)

11-27: ⚠️ Potential issue | 🟠 Major

Avoid predictable temp key path and guarantee cleanup on all exits.

At Line 11/24-27, decrypted key material is written to a fixed path and only removed on success. If import fails, plaintext key can remain on disk.

Safer temp-file handling
-GPG_TEMP_FILE="$AGENIX_DIR/gpg.key"
+GPG_TEMP_FILE="$(mktemp "$AGENIX_DIR/gpg.XXXXXX.key")"
+trap 'rm -f "$GPG_TEMP_FILE"' EXIT
@@
 if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
   "$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null
-  rm -f "$GPG_TEMP_FILE"
   echo "GPG key imported successfully"
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/activation/import-gpg-key.sh` around lines 11 - 27, The script
writes decrypted key material to a predictable path in GPG_TEMP_FILE and only
removes it on successful import, risking leftover plaintext; change to create a
secure temporary file (use mktemp or similar) with restrictive permissions for
the decrypted output, write the RAGE_BIN -d -i output to that temp file, and add
a cleanup trap (trap 'rm -f "$TMPFILE"' EXIT INT TERM) so the temp is removed on
all exits/failures; update references from GPG_TEMP_FILE to the new TMPFILE and
ensure any early-exit paths (including failed import) still trigger the cleanup
before exiting.
config/gemini/activate.sh-9-9 (1)

9-9: ⚠️ Potential issue | 🟠 Major

Use cp -f to keep activation non-interactive.

Line 9 uses plain cp; this violates the repo’s non-interactive shell rule for file operations.

Proposed fix
-  cp "$SETTINGS_JSON" "$HOME/.gemini/settings.json"
+  cp -f "$SETTINGS_JSON" "$HOME/.gemini/settings.json"

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/gemini/activate.sh` at line 9, Replace the interactive copy with a
forced, non-interactive copy: update the cp invocation that copies
"$SETTINGS_JSON" to "$HOME/.gemini/settings.json" by adding the -f flag (i.e.,
use cp -f) so the activation script's copy operation is non-interactive and
won't prompt for confirmations.
config/codex/activate.sh-8-12 (1)

8-12: ⚠️ Potential issue | 🟠 Major

Hooks destination path appears incorrect.

Line 8 creates ~/.codex/hooks, but Lines 11-12 write to ~/.codex/hooks.json instead of inside that directory. This mismatch is likely a path bug.

Proposed fix
 mkdir -p ~/.codex/hooks
 cp -f "$CONFIG_TOML" ~/.codex/config.toml
 chmod 600 ~/.codex/config.toml
-cp -f "$HOOKS_JSON" ~/.codex/hooks.json
-chmod 644 ~/.codex/hooks.json
+cp -f "$HOOKS_JSON" ~/.codex/hooks/hooks.json
+chmod 644 ~/.codex/hooks/hooks.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/codex/activate.sh` around lines 8 - 12, The script creates a hooks
directory (~/.codex/hooks) but then copies the hooks file to
~/.codex/hooks.json, so update the destination to the directory: copy
"$HOOKS_JSON" to ~/.codex/hooks/hooks.json and set its permissions (chmod 644
~/.codex/hooks/hooks.json); keep the CONFIG_TOML handling as-is (CONFIG_TOML ->
~/.codex/config.toml) and ensure references to HOOKS_JSON and the mkdir command
align with the new path.
config/serena/activate.sh-11-11 (1)

11-11: ⚠️ Potential issue | 🟠 Major

Use forced non-interactive copy.

Line 11 should use cp -f to comply with the repo’s non-interactive shell policy.

Proposed fix
-  cp "$SRC" "$DEST"
+  cp -f "$SRC" "$DEST"

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/serena/activate.sh` at line 11, Replace the interactive copy command
with a forced non-interactive copy: update the cp invocation that uses the
variables SRC and DEST (cp "$SRC" "$DEST") to include the -f flag so it becomes
a forced, non-interactive operation (use cp -f "$SRC" "$DEST"). Ensure the
surrounding logic still handles errors/exit codes as before.
hosts/linux/activate-backup-files.sh-8-8 (1)

8-8: ⚠️ Potential issue | 🟠 Major

Use forced non-interactive moves and quote the codex path.

Line 8 and Line 14 should use mv -f per repo policy. Also prefer "$HOME/.codex" on Line 17 for path safety.

Proposed fix
-    mv "$HOME/$file" "$HOME/$file.hm-backup"
+    mv -f "$HOME/$file" "$HOME/$file.hm-backup"
@@
-  mv "$HOME/.openclaw/openclaw.json" "$HOME/.openclaw/openclaw.json.hm-backup"
+  mv -f "$HOME/.openclaw/openclaw.json" "$HOME/.openclaw/openclaw.json.hm-backup"
@@
-find ~/.codex -name "*.hm-backup*" -delete 2>/dev/null || true
+find "$HOME/.codex" -name "*.hm-backup*" -delete 2>/dev/null || true

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

Also applies to: 14-14, 17-17

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hosts/linux/activate-backup-files.sh` at line 8, Change the interactive move
commands to forced non-interactive moves and quote the codex path: replace
occurrences of mv "$HOME/$file" "$HOME/$file.hm-backup" (both spots) with a
non-interactive mv -f variant, and update the codex directory reference to use
the quoted form "$HOME/.codex" to ensure path safety and comply with the repo
policy on file operations.
config/omp/activate.sh-7-10 (1)

7-10: ⚠️ Potential issue | 🟠 Major

Quote HOME-derived paths to avoid word-splitting bugs.

Line 7–Line 10 use unquoted ~ paths. If HOME ever contains spaces, these commands can break.

Proposed fix
-mkdir -p ~/.omp/agent ~/.omp/agent/extensions
-cp -f "$CONFIG_YML" ~/.omp/agent/config.yml
-chmod 644 ~/.omp/agent/config.yml
-ln -sfn "$HOME/dotfiles/node_modules/@oh-my-pi/swarm-extension" ~/.omp/agent/extensions/swarm-extension
+mkdir -p "$HOME/.omp/agent" "$HOME/.omp/agent/extensions"
+cp -f "$CONFIG_YML" "$HOME/.omp/agent/config.yml"
+chmod 644 "$HOME/.omp/agent/config.yml"
+ln -sfn "$HOME/dotfiles/node_modules/@oh-my-pi/swarm-extension" "$HOME/.omp/agent/extensions/swarm-extension"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/omp/activate.sh` around lines 7 - 10, The commands mkdir -p, cp -f,
chmod and ln are using unquoted HOME-derived paths (e.g. ~/.omp/agent,
~/.omp/agent/extensions and $HOME/dotfiles/...) which can break if HOME contains
spaces; change to use explicit "$HOME" and quote all path arguments (e.g. mkdir
-p "$HOME/.omp/agent" "$HOME/.omp/agent/extensions", cp -f "$CONFIG_YML"
"$HOME/.omp/agent/config.yml", chmod 644 "$HOME/.omp/agent/config.yml", ln -sfn
"$HOME/dotfiles/node_modules/@oh-my-pi/swarm-extension"
"$HOME/.omp/agent/extensions/swarm-extension") so that CONFIG_YML and
HOME-derived paths are safely handled in mkdir, cp, chmod and ln.
spec/activate_fnm_spec.sh-35-42 (1)

35-42: ⚠️ Potential issue | 🟠 Major

These assertions can pass on comments instead of the command lines.

grep 'install' and especially grep 'fnm.*default' are broad enough to match the usage/comments in home-manager/programs/fnm/activate.sh, so this spec can stay green even if the actual "$FNM_BIN" install ... or "$FNM_BIN" default ... commands are removed. Match the executable lines more precisely here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/activate_fnm_spec.sh` around lines 35 - 42, The current spec uses broad
grep patterns that can match comments; tighten assertions to search for the
actual executable lines by matching the literal command invocations such as
"$FNM_BIN" install and "$FNM_BIN" default (or the exact line in activate.sh that
invokes FNM) instead of generic 'install' or 'fnm.*default'; update the tests in
spec/activate_fnm_spec.sh to grep for the quoted command pattern (including
"$FNM_BIN" and the subcommand) or anchor the line start so the test only passes
when the real install/default command is present.
home-manager/programs/fnm/default.nix-24-30 (1)

24-30: ⚠️ Potential issue | 🟠 Major

The stable node symlink is still effectively pinned to v22.

This activation now passes ${defaultVersion}, but home-manager/programs/fnm/activate.sh, Lines 21-28, still resolves ~/.local/bin/node from v22* instead of DEFAULT_VERSION. As soon as nodeVersions changes, fnm default and the stable symlink diverge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/programs/fnm/default.nix` around lines 24 - 30, The activation
currently passes defaultVersion from home.activation.fnmSetup but activate.sh
still resolves the stable node symlink by hardcoded glob v22*; update
activate.sh to accept and use the passed DEFAULT_VERSION (third argument from
the activation call) when locating/creating the ~/.local/bin/node stable symlink
(i.e., replace the v22* glob resolution with logic that uses the DEFAULT_VERSION
argument or its derived path), ensuring fnm default and the stable symlink
remain in sync with the defaultVersion provided by home.activation.fnmSetup.
spec/activate_hosts_spec.sh-43-72 (1)

43-72: ⚠️ Potential issue | 🟠 Major

Add a regression check for non-interactive mv -f.

hosts/linux/activate-backup-files.sh, Lines 5-13, still use plain mv, and this suite would stay green. That leaves the extracted activation logic free to violate the repo shell rule and potentially block on confirmation prompts.

Proposed spec hardening
 Describe 'backup behavior'
+It 'uses non-interactive moves'
+When run bash -c "grep 'mv -f' '$SCRIPT'"
+The output should include 'mv -f'
+End
+
 It 'backs up .bashrc'
 When run bash -c "grep '.bashrc' '$SCRIPT'"
 The output should include '.bashrc'
 End

As per coding guidelines "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/activate_hosts_spec.sh` around lines 43 - 72, The activation script
hosts/linux/activate-backup-files.sh is using plain mv (lines ~5-13) which can
block on confirmation; update every mv invocation in that script to use mv -f
(non-interactive) and similarly ensure any cp/rm calls there use their
non-interactive flags (cp -f, rm -f or rm -rf) so file ops never prompt; verify
the updated commands still preserve the existing behavior and adjust the spec if
needed to assert the presence of '-f' in the generated activation script.
spec/activate_config_spec.sh-80-110 (1)

80-110: ⚠️ Potential issue | 🟠 Major

Add assertions for repo-mandated cp -f usage in these config suites.

config/gemini/activate.sh, Lines 7-10, and config/serena/activate.sh, Lines 9-13, currently use plain cp, and nothing in this spec would catch that. Since these tests were added as the regression net for the extracted scripts, they should also guard the required non-interactive copy flags.

As per coding guidelines "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/activate_config_spec.sh` around lines 80 - 110, Add assertions to the
two spec blocks that verify the scripts use non-interactive cp flags: for the
gemini block (where SCRIPT="$PWD/config/gemini/activate.sh") add a test that
greps for 'cp -f' (similar style to the existing grep tests), and for the serena
block (where SCRIPT="$PWD/config/serena/activate.sh") add the same grep
assertion for 'cp -f'; ensure the new It/When run lines mirror the existing
pattern so the spec fails if plain `cp` is used instead of `cp -f`.
named-hosts/kyber/activate-ip-forwarding.sh-34-37 (1)

34-37: ⚠️ Potential issue | 🟠 Major

Persist the desired sysctl contents every run.

This only writes 99-tailscale.conf when the file is missing. If the file already exists with stale or partial contents, activation never repairs it, so the system can drift permanently from the declared state.

Proposed fix
-if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
-  echo 'net.ipv4.ip_forward=1' | run_root_cmd tee /etc/sysctl.d/99-tailscale.conf
-  echo 'net.ipv6.conf.all.forwarding=1' | run_root_cmd tee -a /etc/sysctl.d/99-tailscale.conf
-fi
+{
+  echo 'net.ipv4.ip_forward=1'
+  echo 'net.ipv6.conf.all.forwarding=1'
+} | run_root_cmd tee /etc/sysctl.d/99-tailscale.conf >/dev/null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/kyber/activate-ip-forwarding.sh` around lines 34 - 37, The script
currently only writes /etc/sysctl.d/99-tailscale.conf if it does not exist;
change it so activation enforces the two sysctl settings every run by ensuring
/etc/sysctl.d/99-tailscale.conf contains both "net.ipv4.ip_forward=1" and
"net.ipv6.conf.all.forwarding=1" (either by atomically overwriting the file with
those two lines or by idempotently replacing/adding those keys), using the same
helper run_root_cmd and tee invocation patterns so the activation always repairs
stale or partial contents.
named-hosts/kyber/activate-ip-forwarding.sh-29-33 (1)

29-33: ⚠️ Potential issue | 🟠 Major

IPv6 forwarding is skipped whenever IPv4 is already enabled.

The guard only reads /proc/sys/net/ipv4/ip_forward, but it gates both sysctl -w calls. A machine with IPv4 forwarding already set to 1 and IPv6 forwarding still at 0 stays half-configured after activation.

Proposed fix
-if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
-  echo "Enabling IP forwarding for Tailscale exit node..."
-  run_root_cmd sysctl -w net.ipv4.ip_forward=1
-  run_root_cmd sysctl -w net.ipv6.conf.all.forwarding=1
-fi
+if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
+  echo "Enabling IPv4 forwarding for Tailscale exit node..."
+  run_root_cmd sysctl -w net.ipv4.ip_forward=1
+fi
+
+if [ "$(cat /proc/sys/net/ipv6/conf/all/forwarding)" != "1" ]; then
+  echo "Enabling IPv6 forwarding for Tailscale exit node..."
+  run_root_cmd sysctl -w net.ipv6.conf.all.forwarding=1
+fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/kyber/activate-ip-forwarding.sh` around lines 29 - 33, The
current if guard checks only /proc/sys/net/ipv4/ip_forward and gates both sysctl
calls, leaving IPv6 unchanged if IPv4 is already 1; update the logic to test and
enable IPv4 and IPv6 separately by reading /proc/sys/net/ipv4/ip_forward and
/proc/sys/net/ipv6/conf/all/forwarding, and call run_root_cmd sysctl -w
net.ipv4.ip_forward=1 only when the IPv4 value is not "1" and run_root_cmd
sysctl -w net.ipv6.conf.all.forwarding=1 only when the IPv6 value is not "1"
(preserve existing echo/logging behavior for each change).
home-manager/modules/tailscale/activate-install-service.sh-38-38 (1)

38-38: ⚠️ Potential issue | 🟠 Major

Use forced copy flags for root writes to keep activation non-interactive.

Line 38 and Line 54 use cp without -f. This can become interactive in some environments and breaks the repo’s non-interactive contract for activation scripts.

Suggested patch
-  run_root_cmd cp "$NIX_SERVICE" "$SERVICE_FILE"
+  run_root_cmd cp -f "$NIX_SERVICE" "$SERVICE_FILE"
@@
-  run_root_cmd cp "$TEMP_SUDOERS" "$SUDOERS_FILE"
+  run_root_cmd cp -f "$TEMP_SUDOERS" "$SUDOERS_FILE"

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

Also applies to: 54-54

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/modules/tailscale/activate-install-service.sh` at line 38, The
cp invocations performed via run_root_cmd (specifically the calls that copy
"$NIX_SERVICE" to "$SERVICE_FILE" and the similar copy at the later occurrence)
are missing the non-interactive forced flag and can block activation; update
those calls to use cp -f (i.e., run_root_cmd cp -f "$NIX_SERVICE"
"$SERVICE_FILE") so root writes are forced and non-interactive, ensuring both
the first occurrence around the run_root_cmd copying NIX_SERVICE->SERVICE_FILE
and the second occurrence (the copy at the later location) are changed
consistently.
home-manager/programs/fnm/activate.sh-23-29 (1)

23-29: ⚠️ Potential issue | 🟠 Major

Don’t hardcode v22 when DEFAULT_VERSION is already an input.

Line 25 ignores the passed default version, so the node symlink can drift from the configured default when that value changes.

Suggested patch
-  latest_v22=$(ls -d "$FNM_DIR/node-versions/v22"* 2>/dev/null | head -1)
-  if [ -n "$latest_v22" ] && [ -d "$latest_v22/installation/bin" ]; then
+  latest_default=$(ls -d "$FNM_DIR/node-versions/v${DEFAULT_VERSION}"* 2>/dev/null | head -1)
+  if [ -n "$latest_default" ] && [ -d "$latest_default/installation/bin" ]; then
     mkdir -p "$HOME/.local/bin"
-    ln -sf "$latest_v22/installation/bin/node" "$HOME/.local/bin/node"
+    ln -sf "$latest_default/installation/bin/node" "$HOME/.local/bin/node"
   fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/programs/fnm/activate.sh` around lines 23 - 29, The code
hardcodes "v22" when locating the default Node version; update the logic to use
the DEFAULT_VERSION variable instead: replace the latest_v22 lookup with a
variable that builds the pattern from "$DEFAULT_VERSION" (e.g.
latest_v_default=$(ls -d "$FNM_DIR/node-versions/v${DEFAULT_VERSION}"*
2>/dev/null | head -1)), then proceed to test [ -n "$latest_v_default" ] and [
-d "$latest_v_default/installation/bin" ] and create the symlink from that path
to "$HOME/.local/bin/node"; also rename references to latest_v22 accordingly
(and ensure DEFAULT_VERSION is quoted when interpolated).
named-hosts/kyber/default.nix-36-36 (1)

36-36: ⚠️ Potential issue | 🟠 Major

Inconsistent dry-run guarding in backup activation hook.

Line 36 invokes activate-backup-files.sh without the $DRY_RUN_CMD prefix, while other activation hooks (lines 62, 67, 73–77, 83–89, 122) consistently use it. The backup script contains mv commands without internal dry-run protection, meaning a dry-run of home-manager will still mutate user files instead of being safely skipped.

Apply the fix:

Suggested fix
-              ${pkgs.bash}/bin/bash ${./activate-backup-files.sh}
+              $DRY_RUN_CMD ${pkgs.bash}/bin/bash ${./activate-backup-files.sh}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/kyber/default.nix` at line 36, The activate-backup-files hook
calls ${pkgs.bash}/bin/bash ${./activate-backup-files.sh} without using the
DRY_RUN_CMD prefix, so a home-manager dry-run still executes the script (which
contains mv operations) and mutates files; change the invocation to include the
DRY_RUN_CMD (i.e. prefix the command with ${DRY_RUN_CMD}) so that when
DRY_RUN_CMD is set the script run is a no-op, ensuring activate-backup-files.sh
is guarded consistently with the other activation hooks that use DRY_RUN_CMD.
🟡 Minor comments (3)
named-hosts/kyber/activate-backup-files.sh-8-8 (1)

8-8: ⚠️ Potential issue | 🟡 Minor

Use mv -f for non-interactive activation behavior.

Please force the move to avoid any prompt-driven hangs in automation contexts.

Proposed fix
-    mv "$HOME/$file" "$HOME/$file.hm-backup"
+    mv -f "$HOME/$file" "$HOME/$file.hm-backup"

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/kyber/activate-backup-files.sh` at line 8, The mv invocation in
activate-backup-files.sh uses interactive defaults which can hang automation;
update the move command that references "$HOME/$file" -> "$HOME/$file.hm-backup"
to use the non-interactive forced flag (mv -f) so the rename proceeds without
prompts in scripts and CI.
hosts/linux/default.nix-39-39 (1)

39-39: ⚠️ Potential issue | 🟡 Minor

Delegated backup script uses interactive-prone mv calls.

This activation now routes through hosts/linux/activate-backup-files.sh, where the backup moves are currently mv ... without -f. Please switch those to mv -f to keep activation fully non-interactive.

Proposed patch (in hosts/linux/activate-backup-files.sh)
-    mv "$HOME/$file" "$HOME/$file.hm-backup"
+    mv -f "$HOME/$file" "$HOME/$file.hm-backup"
@@
-  mv "$HOME/.openclaw/openclaw.json" "$HOME/.openclaw/openclaw.json.hm-backup"
+  mv -f "$HOME/.openclaw/openclaw.json" "$HOME/.openclaw/openclaw.json.hm-backup"

As per coding guidelines, "Always use non-interactive flags with file operations (cp -f, mv -f, rm -f, rm -rf, cp -rf) to avoid hanging on confirmation prompts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hosts/linux/default.nix` at line 39, The delegated activation script
hosts/linux/activate-backup-files.sh currently uses interactive-prone move
commands; update every plain mv invocation in that script to use mv -f so the
activation is non-interactive (replace occurrences of "mv " with "mv -f " for
the backup/move operations), ensuring all backup/move paths handled by the
script (including any within functions or loops) use the -f flag.
home-manager/programs/neovim/default.nix-47-50 (1)

47-50: ⚠️ Potential issue | 🟡 Minor

Quote ${packDir} when invoking the extracted script.

packDir expands $HOME at runtime. Without quotes, a home path containing spaces is split into multiple argv entries and activate-build-plugins.sh receives a broken pack directory.

Proposed fix
-    $DRY_RUN_CMD ${pkgs.bash}/bin/bash ${./activate-build-plugins.sh} ${packDir} ${libExt}
+    $DRY_RUN_CMD ${pkgs.bash}/bin/bash ${./activate-build-plugins.sh} "${packDir}" "${libExt}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/programs/neovim/default.nix` around lines 47 - 50, In the
home.activation.buildNvimNativePlugins shell block, the unquoted ${packDir} can
expand to a path with spaces and break the argv passed to
${./activate-build-plugins.sh}; update the invocation inside the heredoc so the
script is called with "${packDir}" (and optionally quote ${libExt} too) to
ensure the pack directory is passed as a single argument; locate the block using
home.activation.buildNvimNativePlugins and the activate-build-plugins.sh
invocation to apply the change.
🧹 Nitpick comments (3)
config/openclaw/hydrate.sh (1)

9-9: Remove redundant mkdir -p "$STATE_DIR" invocation.

Now that Line 9 creates STATE_DIR, the second mkdir -p "$STATE_DIR" at Line 61 is duplicate and can be removed to keep flow cleaner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/openclaw/hydrate.sh` at line 9, Remove the redundant directory
creation: since STATE_DIR is already created earlier in hydrate.sh with mkdir -p
"$STATE_DIR", delete the later duplicate mkdir -p "$STATE_DIR" invocation (the
second occurrence) so the script only creates STATE_DIR once; ensure no other
logic depends on that repeated call and preserve surrounding commands as-is.
hosts/darwin/default.nix (1)

47-47: Consider honoring Home Manager dry-run mode here.

Line 47 runs cleanup unconditionally; prefixing with $DRY_RUN_CMD keeps behavior consistent with other activation hooks during dry runs.

Suggested adjustment
-              ${pkgs.bash}/bin/bash ${./activate-remove-backups.sh}
+              $DRY_RUN_CMD ${pkgs.bash}/bin/bash ${./activate-remove-backups.sh}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hosts/darwin/default.nix` at line 47, The activation hook currently executes
the cleanup script unconditionally; update the activation command that calls
activate-remove-backups.sh to respect Home Manager dry-run mode by prefixing the
invocation with the DRY_RUN_CMD variable (i.e., use $DRY_RUN_CMD before
${./activate-remove-backups.sh}) so the script runs only when not in dry-run,
matching other activation hooks' behavior.
home-manager/modules/tailscale/activate-install-service.sh (1)

50-59: Guard temp-file cleanup with a trap.

TEMP_SUDOERS is only removed at the end; any earlier failure leaves it behind. Add a trap right after creation.

Suggested patch
 TEMP_SUDOERS=$(mktemp)
+cleanup() {
+  rm -f "$TEMP_SUDOERS"
+}
+trap cleanup EXIT
 echo "$SUDOERS_CONTENT" >"$TEMP_SUDOERS"
@@
-rm -f "$TEMP_SUDOERS"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/modules/tailscale/activate-install-service.sh` around lines 50 -
59, TEMP_SUDOERS created by mktemp can be left behind on early exit; after
creating TEMP_SUDOERS (and after ensuring mktemp succeeded) add a trap to remove
it on EXIT/INT/TERM (e.g., trap 'rm -f "$TEMP_SUDOERS"' EXIT) so the temp file
is always cleaned up, and keep the existing rm -f at the end or remove it if
relying solely on the trap; reference TEMP_SUDOERS, mktemp, SUDOERS_CONTENT,
SUDOERS_FILE and run_root_cmd when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Major comments:
In `@config/codex/activate.sh`:
- Around line 8-12: The script creates a hooks directory (~/.codex/hooks) but
then copies the hooks file to ~/.codex/hooks.json, so update the destination to
the directory: copy "$HOOKS_JSON" to ~/.codex/hooks/hooks.json and set its
permissions (chmod 644 ~/.codex/hooks/hooks.json); keep the CONFIG_TOML handling
as-is (CONFIG_TOML -> ~/.codex/config.toml) and ensure references to HOOKS_JSON
and the mkdir command align with the new path.

In `@config/gemini/activate.sh`:
- Line 9: Replace the interactive copy with a forced, non-interactive copy:
update the cp invocation that copies "$SETTINGS_JSON" to
"$HOME/.gemini/settings.json" by adding the -f flag (i.e., use cp -f) so the
activation script's copy operation is non-interactive and won't prompt for
confirmations.

In `@config/k3s/activate.sh`:
- Around line 18-23: The script currently only attempts installation when
SUDO_CMD is set; change the logic to: if SUDO_CMD is available use it, else if
the script is running as root (UID=0) run the commands without sudo, otherwise
print the warning; also make the file copy non-interactive by using cp -f and
ensure mkdir uses -p as before; update references to SUDO_CMD and the commands
that create /etc/rancher/k3s and copy "$HOME/.config/k3s/config.yaml" so they
run under sudo when present or directly when UID=0.

In `@config/omp/activate.sh`:
- Around line 7-10: The commands mkdir -p, cp -f, chmod and ln are using
unquoted HOME-derived paths (e.g. ~/.omp/agent, ~/.omp/agent/extensions and
$HOME/dotfiles/...) which can break if HOME contains spaces; change to use
explicit "$HOME" and quote all path arguments (e.g. mkdir -p "$HOME/.omp/agent"
"$HOME/.omp/agent/extensions", cp -f "$CONFIG_YML"
"$HOME/.omp/agent/config.yml", chmod 644 "$HOME/.omp/agent/config.yml", ln -sfn
"$HOME/dotfiles/node_modules/@oh-my-pi/swarm-extension"
"$HOME/.omp/agent/extensions/swarm-extension") so that CONFIG_YML and
HOME-derived paths are safely handled in mkdir, cp, chmod and ln.

In `@config/serena/activate.sh`:
- Line 11: Replace the interactive copy command with a forced non-interactive
copy: update the cp invocation that uses the variables SRC and DEST (cp "$SRC"
"$DEST") to include the -f flag so it becomes a forced, non-interactive
operation (use cp -f "$SRC" "$DEST"). Ensure the surrounding logic still handles
errors/exit codes as before.

In `@home-manager/activation/deploy-agenix-secret.sh`:
- Around line 20-25: The script currently lets rage write directly to "$DEST"
(subject to the current umask) before chmod 0600 is applied, risking brief
exposure; change the flow to decrypt into a securely-created temporary file in
the same directory (create with mktemp or set umask 077 and create temp with
restricted perms), ensure the temp is chmod 0600 before invoking RAGE_BIN -o
"$TMP", on success atomically move the temp into "$DEST" (mv -f) and remove the
temp on failure; reference the variables RAGE_BIN, IDENTITY_KEY, SECRET_FILE and
DEST and ensure cleanup/exit codes are preserved.

In `@home-manager/activation/import-gpg-key.sh`:
- Around line 11-27: The script writes decrypted key material to a predictable
path in GPG_TEMP_FILE and only removes it on successful import, risking leftover
plaintext; change to create a secure temporary file (use mktemp or similar) with
restrictive permissions for the decrypted output, write the RAGE_BIN -d -i
output to that temp file, and add a cleanup trap (trap 'rm -f "$TMPFILE"' EXIT
INT TERM) so the temp is removed on all exits/failures; update references from
GPG_TEMP_FILE to the new TMPFILE and ensure any early-exit paths (including
failed import) still trigger the cleanup before exiting.

In `@home-manager/modules/cargo-globals/default.nix`:
- Around line 28-29: The activation exports (RUSTUP_BIN and conditional
SYSTEMCTL_BIN) are not propagated into the
systemd.user.services.install-cargo-globals unit, so the user service runs
install-cargo-globals.sh without RUSTUP_BIN; update the service definition
(install-cargo-globals) to export the same environment into the unit—e.g., add
Environment=RUSTUP_BIN=${pkgs.rustup}/bin/rustup and, when !isDarwin,
Environment=SYSTEMCTL_BIN=${pkgs.systemd}/bin/systemctl (or point
EnvironmentFile to the same generated env file) so install-cargo-globals.sh sees
the same variables during login as during activation.

In `@home-manager/modules/cargo-globals/install-cargo-globals.sh`:
- Around line 11-15: The rustup network operations (invocations of "$RUSTUP_BIN"
toolchain install/default) must be moved to run after the existing offline guard
check and protected with timeouts so activation won’t hang on flaky or offline
systems; relocate the whole if [ -n "${RUSTUP_BIN:-}" ]; then ... fi block to
after the offline connectivity check, and wrap the rustup calls with a timeout
(e.g., timeout 10s) and preserve the || true fallback so failures don’t break
activation.

In `@home-manager/modules/tailscale/activate-install-service.sh`:
- Line 38: The cp invocations performed via run_root_cmd (specifically the calls
that copy "$NIX_SERVICE" to "$SERVICE_FILE" and the similar copy at the later
occurrence) are missing the non-interactive forced flag and can block
activation; update those calls to use cp -f (i.e., run_root_cmd cp -f
"$NIX_SERVICE" "$SERVICE_FILE") so root writes are forced and non-interactive,
ensuring both the first occurrence around the run_root_cmd copying
NIX_SERVICE->SERVICE_FILE and the second occurrence (the copy at the later
location) are changed consistently.

In `@home-manager/programs/fnm/activate.sh`:
- Around line 23-29: The code hardcodes "v22" when locating the default Node
version; update the logic to use the DEFAULT_VERSION variable instead: replace
the latest_v22 lookup with a variable that builds the pattern from
"$DEFAULT_VERSION" (e.g. latest_v_default=$(ls -d
"$FNM_DIR/node-versions/v${DEFAULT_VERSION}"* 2>/dev/null | head -1)), then
proceed to test [ -n "$latest_v_default" ] and [ -d
"$latest_v_default/installation/bin" ] and create the symlink from that path to
"$HOME/.local/bin/node"; also rename references to latest_v22 accordingly (and
ensure DEFAULT_VERSION is quoted when interpolated).

In `@home-manager/programs/fnm/default.nix`:
- Around line 24-30: The activation currently passes defaultVersion from
home.activation.fnmSetup but activate.sh still resolves the stable node symlink
by hardcoded glob v22*; update activate.sh to accept and use the passed
DEFAULT_VERSION (third argument from the activation call) when locating/creating
the ~/.local/bin/node stable symlink (i.e., replace the v22* glob resolution
with logic that uses the DEFAULT_VERSION argument or its derived path), ensuring
fnm default and the stable symlink remain in sync with the defaultVersion
provided by home.activation.fnmSetup.

In `@hosts/linux/activate-backup-files.sh`:
- Line 8: Change the interactive move commands to forced non-interactive moves
and quote the codex path: replace occurrences of mv "$HOME/$file"
"$HOME/$file.hm-backup" (both spots) with a non-interactive mv -f variant, and
update the codex directory reference to use the quoted form "$HOME/.codex" to
ensure path safety and comply with the repo policy on file operations.

In `@named-hosts/kyber/activate-ip-forwarding.sh`:
- Around line 34-37: The script currently only writes
/etc/sysctl.d/99-tailscale.conf if it does not exist; change it so activation
enforces the two sysctl settings every run by ensuring
/etc/sysctl.d/99-tailscale.conf contains both "net.ipv4.ip_forward=1" and
"net.ipv6.conf.all.forwarding=1" (either by atomically overwriting the file with
those two lines or by idempotently replacing/adding those keys), using the same
helper run_root_cmd and tee invocation patterns so the activation always repairs
stale or partial contents.
- Around line 29-33: The current if guard checks only
/proc/sys/net/ipv4/ip_forward and gates both sysctl calls, leaving IPv6
unchanged if IPv4 is already 1; update the logic to test and enable IPv4 and
IPv6 separately by reading /proc/sys/net/ipv4/ip_forward and
/proc/sys/net/ipv6/conf/all/forwarding, and call run_root_cmd sysctl -w
net.ipv4.ip_forward=1 only when the IPv4 value is not "1" and run_root_cmd
sysctl -w net.ipv6.conf.all.forwarding=1 only when the IPv6 value is not "1"
(preserve existing echo/logging behavior for each change).

In `@named-hosts/kyber/default.nix`:
- Line 36: The activate-backup-files hook calls ${pkgs.bash}/bin/bash
${./activate-backup-files.sh} without using the DRY_RUN_CMD prefix, so a
home-manager dry-run still executes the script (which contains mv operations)
and mutates files; change the invocation to include the DRY_RUN_CMD (i.e. prefix
the command with ${DRY_RUN_CMD}) so that when DRY_RUN_CMD is set the script run
is a no-op, ensuring activate-backup-files.sh is guarded consistently with the
other activation hooks that use DRY_RUN_CMD.

In `@named-hosts/matic/default.nix`:
- Around line 520-523: The deploy call to deploy-agenix-secret.sh is passing
../galactica/keys/id_ed25519.age but this host's agenix mapping and the kyber
host expect keys named id_github.age for ~/.ssh/id_ed25519_github; update the
secret argument passed to deploy-agenix-secret.sh (the third positional arg in
the invocation using DRY_RUN_CMD and ${pkgs.bash}/bin/bash) to reference the
consistent secret name (keys/id_github.age) so the agenix mapping for
id_ed25519_github matches across hosts.

In `@scripts/check-nix-inline-scripts.sh`:
- Line 49: The activation-block end matcher in the body_started conditional only
matches closing sequences that include a trailing semicolon (`'';`) which misses
cases where activation blocks end with a bare `''` and the `;` appears later
(e.g., inside `mkIf` wrappers); update the pattern used in the `body_started &&
/^[[:space:]]*'"''"';/` test to also accept a bare `''` closer (and optional
following `;`) so the `body_started` branch correctly detects the end of
`home.activation` blocks; locate the `body_started` conditional and its regex in
scripts/check-nix-inline-scripts.sh and broaden the regex to match `''` with or
without an immediate semicolon.

In `@spec/activate_config_spec.sh`:
- Around line 80-110: Add assertions to the two spec blocks that verify the
scripts use non-interactive cp flags: for the gemini block (where
SCRIPT="$PWD/config/gemini/activate.sh") add a test that greps for 'cp -f'
(similar style to the existing grep tests), and for the serena block (where
SCRIPT="$PWD/config/serena/activate.sh") add the same grep assertion for 'cp
-f'; ensure the new It/When run lines mirror the existing pattern so the spec
fails if plain `cp` is used instead of `cp -f`.

In `@spec/activate_fnm_spec.sh`:
- Around line 35-42: The current spec uses broad grep patterns that can match
comments; tighten assertions to search for the actual executable lines by
matching the literal command invocations such as "$FNM_BIN" install and
"$FNM_BIN" default (or the exact line in activate.sh that invokes FNM) instead
of generic 'install' or 'fnm.*default'; update the tests in
spec/activate_fnm_spec.sh to grep for the quoted command pattern (including
"$FNM_BIN" and the subcommand) or anchor the line start so the test only passes
when the real install/default command is present.

In `@spec/activate_hosts_spec.sh`:
- Around line 43-72: The activation script hosts/linux/activate-backup-files.sh
is using plain mv (lines ~5-13) which can block on confirmation; update every mv
invocation in that script to use mv -f (non-interactive) and similarly ensure
any cp/rm calls there use their non-interactive flags (cp -f, rm -f or rm -rf)
so file ops never prompt; verify the updated commands still preserve the
existing behavior and adjust the spec if needed to assert the presence of '-f'
in the generated activation script.

---

Minor comments:
In `@home-manager/programs/neovim/default.nix`:
- Around line 47-50: In the home.activation.buildNvimNativePlugins shell block,
the unquoted ${packDir} can expand to a path with spaces and break the argv
passed to ${./activate-build-plugins.sh}; update the invocation inside the
heredoc so the script is called with "${packDir}" (and optionally quote
${libExt} too) to ensure the pack directory is passed as a single argument;
locate the block using home.activation.buildNvimNativePlugins and the
activate-build-plugins.sh invocation to apply the change.

In `@hosts/linux/default.nix`:
- Line 39: The delegated activation script hosts/linux/activate-backup-files.sh
currently uses interactive-prone move commands; update every plain mv invocation
in that script to use mv -f so the activation is non-interactive (replace
occurrences of "mv " with "mv -f " for the backup/move operations), ensuring all
backup/move paths handled by the script (including any within functions or
loops) use the -f flag.

In `@named-hosts/kyber/activate-backup-files.sh`:
- Line 8: The mv invocation in activate-backup-files.sh uses interactive
defaults which can hang automation; update the move command that references
"$HOME/$file" -> "$HOME/$file.hm-backup" to use the non-interactive forced flag
(mv -f) so the rename proceeds without prompts in scripts and CI.

---

Nitpick comments:
In `@config/openclaw/hydrate.sh`:
- Line 9: Remove the redundant directory creation: since STATE_DIR is already
created earlier in hydrate.sh with mkdir -p "$STATE_DIR", delete the later
duplicate mkdir -p "$STATE_DIR" invocation (the second occurrence) so the script
only creates STATE_DIR once; ensure no other logic depends on that repeated call
and preserve surrounding commands as-is.

In `@home-manager/modules/tailscale/activate-install-service.sh`:
- Around line 50-59: TEMP_SUDOERS created by mktemp can be left behind on early
exit; after creating TEMP_SUDOERS (and after ensuring mktemp succeeded) add a
trap to remove it on EXIT/INT/TERM (e.g., trap 'rm -f "$TEMP_SUDOERS"' EXIT) so
the temp file is always cleaned up, and keep the existing rm -f at the end or
remove it if relying solely on the trap; reference TEMP_SUDOERS, mktemp,
SUDOERS_CONTENT, SUDOERS_FILE and run_root_cmd when making the change.

In `@hosts/darwin/default.nix`:
- Line 47: The activation hook currently executes the cleanup script
unconditionally; update the activation command that calls
activate-remove-backups.sh to respect Home Manager dry-run mode by prefixing the
invocation with the DRY_RUN_CMD variable (i.e., use $DRY_RUN_CMD before
${./activate-remove-backups.sh}) so the script runs only when not in dry-run,
matching other activation hooks' behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09a03d4b-7dcc-4149-9a6b-51200b35611b

📥 Commits

Reviewing files that changed from the base of the PR and between e609bb6 and f7b7738.

📒 Files selected for processing (62)
  • config/claude/activate.sh
  • config/claude/default.nix
  • config/codex/activate.sh
  • config/codex/default.nix
  • config/cursor/activate.sh
  • config/cursor/default.nix
  • config/gemini/activate.sh
  • config/gemini/default.nix
  • config/k3s/activate.sh
  • config/k3s/default.nix
  • config/omp/activate.sh
  • config/omp/default.nix
  • config/openclaw/default.nix
  • config/openclaw/hydrate.sh
  • config/serena/activate.sh
  • config/serena/default.nix
  • home-manager/activation/deploy-agenix-secret.sh
  • home-manager/activation/ensure-directory.sh
  • home-manager/activation/import-gpg-key.sh
  • home-manager/modules/bin-shells/activate.sh
  • home-manager/modules/bin-shells/default.nix
  • home-manager/modules/cargo-globals/default.nix
  • home-manager/modules/cargo-globals/install-cargo-globals.sh
  • home-manager/modules/npm-globals/default.nix
  • home-manager/modules/npm-globals/install-npm-globals.sh
  • home-manager/modules/openclaw/activate.sh
  • home-manager/modules/openclaw/default.nix
  • home-manager/modules/paperclip/activate.sh
  • home-manager/modules/paperclip/default.nix
  • home-manager/modules/tailscale/activate-create-dirs.sh
  • home-manager/modules/tailscale/activate-install-service.sh
  • home-manager/modules/tailscale/default.nix
  • home-manager/modules/uv-globals/default.nix
  • home-manager/modules/uv-globals/install-uv-globals.sh
  • home-manager/programs/fnm/activate.sh
  • home-manager/programs/fnm/default.nix
  • home-manager/programs/neovim/activate-build-plugins.sh
  • home-manager/programs/neovim/activate-copy-pack-lock.sh
  • home-manager/programs/neovim/default.nix
  • hosts/darwin/activate-remove-backups.sh
  • hosts/darwin/default.nix
  • hosts/linux/activate-backup-files.sh
  • hosts/linux/default.nix
  • named-hosts/kyber/activate-backup-files.sh
  • named-hosts/kyber/activate-ip-forwarding.sh
  • named-hosts/kyber/default.nix
  • named-hosts/matic/default.nix
  • scripts/check-nix-inline-scripts.sh
  • spec/activate_bin_shells_spec.sh
  • spec/activate_config_spec.sh
  • spec/activate_fnm_spec.sh
  • spec/activate_hosts_spec.sh
  • spec/activate_k3s_spec.sh
  • spec/activate_kyber_spec.sh
  • spec/activate_neovim_spec.sh
  • spec/activate_paperclip_openclaw_spec.sh
  • spec/activate_tailscale_spec.sh
  • spec/activation_shared_spec.sh
  • spec/cargo_globals_spec.sh
  • spec/coverage_spec.sh
  • spec/npm_globals_spec.sh
  • spec/uv_globals_spec.sh
💤 Files with no reviewable changes (1)
  • config/openclaw/default.nix

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/check-nix-inline-scripts.sh">

<violation number="1" location="scripts/check-nix-inline-scripts.sh:71">
P2: The new `^\$\{` allowlist is too broad and can let direct inline activation commands bypass linting if they start with Nix interpolation.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread scripts/check-nix-inline-scripts.sh Outdated
Replace builtins.toJSON inline generation with tracked config.json
files and activation scripts. git_path and home directory are injected
at activation time via jq/sed.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="config/obsidian/default.nix">

<violation number="1" location="config/obsidian/default.nix:17">
P2: Quote `config.home.homeDirectory` in the activation command to prevent shell argument splitting for paths with spaces.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread config/obsidian/default.nix Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 33 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/check-nix-inline-scripts.sh">

<violation number="1" location="scripts/check-nix-inline-scripts.sh:60">
P2: Iterating `split()` results with `for (i in lines)` makes validation order-unsafe; this can falsely flag valid multi-line bash delegations depending on awk key iteration order.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

saw_content = 0
split(body, lines, "\n")

for (i in lines) {

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Iterating split() results with for (i in lines) makes validation order-unsafe; this can falsely flag valid multi-line bash delegations depending on awk key iteration order.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-nix-inline-scripts.sh, line 60:

<comment>Iterating `split()` results with `for (i in lines)` makes validation order-unsafe; this can falsely flag valid multi-line bash delegations depending on awk key iteration order.</comment>

<file context>
@@ -29,66 +29,74 @@ python_violations=$(grep -rn \
+      saw_content = 0
+      split(body, lines, "\n")
+
+      for (i in lines) {
+        line = trim(lines[i])
+
</file context>
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
config/git-ai/default.nix (1)

3-5: Minor DAG access inconsistency with other modules.

This module uses lib.hm.dag.entryAfter while config/obsidian/default.nix uses config.lib.dag.entryAfter. Both work, but config.lib.dag is the more canonical Home Manager pattern. Consider aligning for consistency across activation modules.

♻️ Optional consistency fix
-  home.activation.gitAiConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
+  home.activation.gitAiConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''

Note: This would also require adding config to the module arguments: { config, pkgs, lib, ... }:

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/git-ai/default.nix` around lines 3 - 5, The module uses
lib.hm.dag.entryAfter for the activation DAG but other modules use the canonical
config.lib.dag.entryAfter; change the invocation to config.lib.dag.entryAfter
and update the module argument list to include config (e.g. add config to the
parameter tuple) so the entryAfter call resolves from config.lib.dag instead of
lib.hm.dag, keeping the rest of the activation string unchanged.
home-manager/modules/tailscale/default.nix (1)

210-218: LGTM! Consider adding a platform guard (optional).

The refactoring correctly delegates systemd service installation to the external script. The context snippets confirm the script properly handles sudo/doas detection and systemd operations.

One optional consideration: installSystemService defaults to true but the underlying script targets systemd, which doesn't exist on macOS/Darwin. Users on Darwin would need to explicitly set installSystemService = false. If Darwin users are expected, you could wrap the activation with a platform check or add early exit logic in the script.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/modules/tailscale/default.nix` around lines 210 - 218, Wrap the
existing activation that runs activate-install-service.sh in a Darwin/platform
guard so it doesn't run on macOS where systemd isn't present: modify the mkIf
condition around home.activation.installTailscaleService (which currently uses
cfg.installSystemService) to also check the platform (e.g.,
config.system.isLinux or !config.system.isDarwin) or alternatively add an
early-return guard inside activate-install-service.sh that no-ops on Darwin;
ensure references to tailscaledServiceFile and config.home.homeDirectory remain
passed unchanged when the guard allows execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Makefile`:
- Line 421: The nix-build target currently depends on nix-trust which makes
ordinary builds mutate system state; remove nix-trust from the nix-build
prerequisite list so nix-build depends only on nix-connect (i.e., change the
target from "nix-build: nix-connect nix-trust" to "nix-build: nix-connect"),
leaving the standalone nix-trust target unchanged for explicit privileged
operations; update any comments or README references if they mention nix-build
invoking trust changes.

In `@named-hosts/matic/default.nix`:
- Around line 530-536: The import helper leaves the decrypted private key under
${AGENIX_DIR} when gpg --import fails; modify import-gpg-key.sh so the decrypted
file is created in a temporary path (or the existing AGENIX_DIR filename) and
ensure it is always removed on exit by installing a trap handler (trap 'rm -f
"$DECRYPTED_FILE"' EXIT) that runs regardless of success, and explicitly call
the cleanup before any early returns/errors after the gpg --import step; update
references in the script (the decryption step, the variable that holds the
decrypted filename, and the gpg --import invocation) to use the temp variable so
the trap can reliably remove the plaintext file.

In `@scripts/check-nix-inline-scripts.sh`:
- Around line 69-70: The current whitelist skips any line that starts with
${lib.optionalString...} without validating the interpolated payload, allowing
inline shell like ${lib.optionalString cond ''mkdir -p "$HOME/.cache"''}; update
the check so when a line matches /^\$\{lib\.optionalString/ you parse the
interpolated string argument (capture the quoted payload inside the
${lib.optionalString ...} wrapper) and only allow the line to continue if that
payload is empty or contains no shell metacharacters (for example: ';', '&',
'|', '`', '$(', '>', '<') — otherwise treat it as disallowed inline shell in
home.activation. Ensure the regex targets the quoted payload and applies the
metacharacter check before continuing.
- Around line 55-80: The loop in block_has_violation relies on processing lines
in original order but uses "for (i in lines)" which iterates in arbitrary order
in AWK; change to an index-based loop by capturing the split count (n =
split(body, lines, "\n")) and iterate for (i = 1; i <= n; i++) so the code
examining is_bash_delegate(...) and subsequent continuation checks
(is_single_arg(...)) see the bash delegation before its args; replace the
existing for (i in lines) construct accordingly while keeping the existing
body/trim logic and variables saw_bash_call and saw_content.

In `@spec/coverage_spec.sh`:
- Around line 416-417: The coverage spec is missing explicit assertions for the
two scripts listed in covered_scripts; add "has spec file for" assertions for
scripts/sync-codex-security.sh and scripts/sync-rtk-rewrite.sh in the same "has
spec file for …" section of spec/coverage_spec.sh so the test fails if either
corresponding spec file is removed (refer to the existing pattern used for other
scripts in that section to match wording and placement).

---

Nitpick comments:
In `@config/git-ai/default.nix`:
- Around line 3-5: The module uses lib.hm.dag.entryAfter for the activation DAG
but other modules use the canonical config.lib.dag.entryAfter; change the
invocation to config.lib.dag.entryAfter and update the module argument list to
include config (e.g. add config to the parameter tuple) so the entryAfter call
resolves from config.lib.dag instead of lib.hm.dag, keeping the rest of the
activation string unchanged.

In `@home-manager/modules/tailscale/default.nix`:
- Around line 210-218: Wrap the existing activation that runs
activate-install-service.sh in a Darwin/platform guard so it doesn't run on
macOS where systemd isn't present: modify the mkIf condition around
home.activation.installTailscaleService (which currently uses
cfg.installSystemService) to also check the platform (e.g.,
config.system.isLinux or !config.system.isDarwin) or alternatively add an
early-return guard inside activate-install-service.sh that no-ops on Darwin;
ensure references to tailscaledServiceFile and config.home.homeDirectory remain
passed unchanged when the guard allows execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8adad49e-3a97-4bb1-99ae-f4a6639956d6

📥 Commits

Reviewing files that changed from the base of the PR and between f7b7738 and 533f320.

📒 Files selected for processing (38)
  • Makefile
  • config/claude/default.nix
  • config/codex/default.nix
  • config/cursor/default.nix
  • config/gemini/default.nix
  • config/git-ai/activate.sh
  • config/git-ai/default.nix
  • config/k3s/default.nix
  • config/obsidian/activate.sh
  • config/obsidian/default.nix
  • config/obsidian/obsidian.json
  • config/omp/default.nix
  • config/openclaw/default.nix
  • config/serena/default.nix
  • home-manager/modules/bin-shells/default.nix
  • home-manager/modules/cargo-globals/default.nix
  • home-manager/modules/local-binaries/default.nix
  • home-manager/modules/npm-globals/default.nix
  • home-manager/modules/openclaw/default.nix
  • home-manager/modules/paperclip/default.nix
  • home-manager/modules/tailscale/default.nix
  • home-manager/modules/uv-globals/default.nix
  • home-manager/packages/default.nix
  • home-manager/programs/fnm/default.nix
  • home-manager/programs/neovim/default.nix
  • hosts/darwin/default.nix
  • hosts/linux/default.nix
  • named-hosts/kyber/default.nix
  • named-hosts/matic/default.nix
  • scripts/check-nix-inline-scripts.sh
  • spec/activate_config_spec.sh
  • spec/activate_fnm_spec.sh
  • spec/activate_kyber_spec.sh
  • spec/activate_neovim_spec.sh
  • spec/activate_paperclip_openclaw_spec.sh
  • spec/activation_shared_spec.sh
  • spec/check_nix_inline_scripts_spec.sh
  • spec/coverage_spec.sh
✅ Files skipped from review due to trivial changes (6)
  • home-manager/packages/default.nix
  • home-manager/modules/local-binaries/default.nix
  • config/obsidian/obsidian.json
  • spec/activate_fnm_spec.sh
  • spec/activate_neovim_spec.sh
  • spec/activation_shared_spec.sh
🚧 Files skipped from review as they are similar to previous changes (19)
  • config/openclaw/default.nix
  • home-manager/modules/npm-globals/default.nix
  • config/cursor/default.nix
  • config/claude/default.nix
  • config/serena/default.nix
  • config/codex/default.nix
  • home-manager/modules/paperclip/default.nix
  • config/k3s/default.nix
  • home-manager/modules/openclaw/default.nix
  • hosts/linux/default.nix
  • hosts/darwin/default.nix
  • home-manager/modules/bin-shells/default.nix
  • config/omp/default.nix
  • home-manager/modules/cargo-globals/default.nix
  • spec/activate_paperclip_openclaw_spec.sh
  • home-manager/programs/fnm/default.nix
  • spec/activate_config_spec.sh
  • named-hosts/kyber/default.nix
  • spec/activate_kyber_spec.sh

Comment thread Makefile

.PHONY: nix-build
nix-build: nix-connect ## Build Nix configuration.
nix-build: nix-connect nix-trust ## Build Nix configuration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep nix-build read-only.

Adding nix-trust here means a normal build can now rewrite /etc/nix/nix.conf and restart nix-daemon via the nix-trust target at Lines 377-385. That turns make build/make install into a privileged, system-mutating step and will fail for users who can still run nix build but cannot or should not modify daemon trust on every build.

Suggested change
-nix-build: nix-connect nix-trust ## Build Nix configuration.
+nix-build: nix-connect ## Build Nix configuration.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nix-build: nix-connect nix-trust ## Build Nix configuration.
nix-build: nix-connect ## Build Nix configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` at line 421, The nix-build target currently depends on nix-trust
which makes ordinary builds mutate system state; remove nix-trust from the
nix-build prerequisite list so nix-build depends only on nix-connect (i.e.,
change the target from "nix-build: nix-connect nix-trust" to "nix-build:
nix-connect"), leaving the standalone nix-trust target unchanged for explicit
privileged operations; update any comments or README references if they mention
nix-build invoking trust changes.

Comment on lines +530 to +536
$DRY_RUN_CMD ${pkgs.bash}/bin/bash "${../../home-manager/activation/import-gpg-key.sh}" \
"${config.home.homeDirectory}/dotfiles/named-hosts/galactica/keys/gpg.age" \
"${config.home.homeDirectory}/.ssh/id_ed25519" \
"${config.home.homeDirectory}/.config/agenix" \
"${pkgs.rage}/bin/rage" \
"${pkgs.gnupg}/bin/gpg" \
"C2E97FCFF482925D"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid leaving the decrypted GPG key on disk if import fails.

This activation now depends on home-manager/activation/import-gpg-key.sh, and that helper decrypts the private key to a fixed file under ${AGENIX_DIR} before running gpg --import. If the import errors, the plaintext key is never removed because cleanup only happens on the success path.

Suggested fix in home-manager/activation/import-gpg-key.sh
 set -euo pipefail
 GPG_SECRET_FILE="$1"
 IDENTITY_KEY="$2"
 AGENIX_DIR="$3"
 RAGE_BIN="$4"
 GPG_BIN="$5"
 KEY_FINGERPRINT="$6"
-GPG_TEMP_FILE="$AGENIX_DIR/gpg.key"

 mkdir -p "$AGENIX_DIR"
+GPG_TEMP_FILE="$(mktemp "$AGENIX_DIR/gpg.XXXXXX")"
+cleanup() {
+  rm -f "$GPG_TEMP_FILE"
+}
+trap cleanup EXIT

 if [[ ! -f $GPG_SECRET_FILE ]]; then
   exit 0
 fi

 if "$GPG_BIN" --list-secret-keys 2>/dev/null | grep -q "$KEY_FINGERPRINT"; then
   exit 0
 fi

 echo "Importing GPG key from agenix..."
 if "$RAGE_BIN" -d -i "$IDENTITY_KEY" -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
   "$GPG_BIN" --batch --import "$GPG_TEMP_FILE" 2>/dev/null
-  rm -f "$GPG_TEMP_FILE"
   echo "GPG key imported successfully"
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/matic/default.nix` around lines 530 - 536, The import helper
leaves the decrypted private key under ${AGENIX_DIR} when gpg --import fails;
modify import-gpg-key.sh so the decrypted file is created in a temporary path
(or the existing AGENIX_DIR filename) and ensure it is always removed on exit by
installing a trap handler (trap 'rm -f "$DECRYPTED_FILE"' EXIT) that runs
regardless of success, and explicitly call the cleanup before any early
returns/errors after the gpg --import step; update references in the script (the
decryption step, the variable that holds the decrypted filename, and the gpg
--import invocation) to use the temp variable so the trap can reliably remove
the plaintext file.

Comment on lines +55 to +80
function block_has_violation( i, line, saw_bash_call, saw_content) {
saw_bash_call = 0
saw_content = 0
split(body, lines, "\n")

for (i in lines) {
line = trim(lines[i])

if (line == "" || line ~ /^#/) continue
saw_content = 1

if (line ~ /&&|;|`|[<>]/) return 1
if (line ~ /\|/ && line !~ /[[:space:]]\|\|[[:space:]]+true[[:space:]]*\\?$/) return 1

if (line ~ /^export /) continue
if (line ~ /^\$\{lib\.optionalString/) continue
if (is_bash_delegate(line)) {
saw_bash_call = 1
continue
}
if (saw_bash_call && is_single_arg(line)) continue

return 1
}

return saw_content && !saw_bash_call

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/check-nix-inline-scripts.sh | sed -n '30,110p'

Repository: shunkakinoki/dotfiles

Length of output: 3002


Iterate activation lines in index order, not for (i in lines).

This logic depends on seeing the bash delegation before its continuation arguments, but awk does not guarantee iteration order for for (i in array). On a different awk implementation, the same activation block can be accepted or rejected inconsistently.

Suggested change
-      split(body, lines, "\n")
-
-      for (i in lines) {
+      line_count = split(body, lines, "\n")
+
+      for (i = 1; i <= line_count; i++) {
         line = trim(lines[i])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function block_has_violation( i, line, saw_bash_call, saw_content) {
saw_bash_call = 0
saw_content = 0
split(body, lines, "\n")
for (i in lines) {
line = trim(lines[i])
if (line == "" || line ~ /^#/) continue
saw_content = 1
if (line ~ /&&|;|`|[<>]/) return 1
if (line ~ /\|/ && line !~ /[[:space:]]\|\|[[:space:]]+true[[:space:]]*\\?$/) return 1
if (line ~ /^export /) continue
if (line ~ /^\$\{lib\.optionalString/) continue
if (is_bash_delegate(line)) {
saw_bash_call = 1
continue
}
if (saw_bash_call && is_single_arg(line)) continue
return 1
}
return saw_content && !saw_bash_call
function block_has_violation( i, line, saw_bash_call, saw_content) {
saw_bash_call = 0
saw_content = 0
line_count = split(body, lines, "\n")
for (i = 1; i <= line_count; i++) {
line = trim(lines[i])
if (line == "" || line ~ /^#/) continue
saw_content = 1
if (line ~ /&&|;|`|[<>]/) return 1
if (line ~ /\|/ && line !~ /[[:space:]]\|\|[[:space:]]+true[[:space:]]*\\?$/) return 1
if (line ~ /^export /) continue
if (line ~ /^\$\{lib\.optionalString/) continue
if (is_bash_delegate(line)) {
saw_bash_call = 1
continue
}
if (saw_bash_call && is_single_arg(line)) continue
return 1
}
return saw_content && !saw_bash_call
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-nix-inline-scripts.sh` around lines 55 - 80, The loop in
block_has_violation relies on processing lines in original order but uses "for
(i in lines)" which iterates in arbitrary order in AWK; change to an index-based
loop by capturing the split count (n = split(body, lines, "\n")) and iterate for
(i = 1; i <= n; i++) so the code examining is_bash_delegate(...) and subsequent
continuation checks (is_single_arg(...)) see the bash delegation before its
args; replace the existing for (i in lines) construct accordingly while keeping
the existing body/trim logic and variables saw_bash_call and saw_content.

Comment on lines +69 to +70
if (line ~ /^export /) continue
if (line ~ /^\$\{lib\.optionalString/) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only allow lib.optionalString exports here.

if (line ~ /^\$\{lib\.optionalString/) continue is too broad: ${lib.optionalString cond ''mkdir -p "$HOME/.cache"''} would currently pass even though this checker is supposed to ban inline shell in home.activation blocks. The whitelist needs to inspect the interpolated payload, not just the wrapper.

Suggested change
+    function is_optional_export(line) {
+      return line ~ /^\$\{lib\.optionalString/ && line ~ /''export /
+    }
+
     function block_has_violation(  i, line, saw_bash_call, saw_content) {
@@
-        if (line ~ /^\$\{lib\.optionalString/) continue
+        if (is_optional_export(line)) continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (line ~ /^export /) continue
if (line ~ /^\$\{lib\.optionalString/) continue
function is_optional_export(line) {
return line ~ /^\$\{lib\.optionalString/ && line ~ /''export /
}
function block_has_violation( i, line, saw_bash_call, saw_content) {
if (line ~ /^export /) continue
if (is_optional_export(line)) continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-nix-inline-scripts.sh` around lines 69 - 70, The current
whitelist skips any line that starts with ${lib.optionalString...} without
validating the interpolated payload, allowing inline shell like
${lib.optionalString cond ''mkdir -p "$HOME/.cache"''}; update the check so when
a line matches /^\$\{lib\.optionalString/ you parse the interpolated string
argument (capture the quoted payload inside the ${lib.optionalString ...}
wrapper) and only allow the line to continue if that payload is empty or
contains no shell metacharacters (for example: ';', '&', '|', '`', '$(', '>',
'<') — otherwise treat it as disallowed inline shell in home.activation. Ensure
the regex targets the quoted payload and applies the metacharacter check before
continuing.

Comment thread spec/coverage_spec.sh
Comment on lines +416 to +417
scripts/sync-codex-security.sh
scripts/sync-rtk-rewrite.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add the matching spec-file assertions for these two scripts.

scripts/sync-codex-security.sh and scripts/sync-rtk-rewrite.sh are back in covered_scripts, but the earlier “has spec file for …” section still has no explicit assertion for either one. If one of their spec files is deleted, this coverage spec will no longer catch it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/coverage_spec.sh` around lines 416 - 417, The coverage spec is missing
explicit assertions for the two scripts listed in covered_scripts; add "has spec
file for" assertions for scripts/sync-codex-security.sh and
scripts/sync-rtk-rewrite.sh in the same "has spec file for …" section of
spec/coverage_spec.sh so the test fails if either corresponding spec file is
removed (refer to the existing pattern used for other scripts in that section to
match wording and placement).

@cubic-dev-ai

cubic-dev-ai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

@shunkakinoki
shunkakinoki merged commit e28fe8b into main Apr 12, 2026
15 checks passed
@shunkakinoki
shunkakinoki deleted the refactor/extract-activation-scripts branch April 12, 2026 11:08
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