nixos/hosts/FredOS-Rescue.nix
rope b960a6ccbf Add FredOS-Rescue: Claude Code rescue USB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:22:14 +01:00

102 lines
3.9 KiB
Nix

# FredOS-Rescue — live USB that boots into Claude Code for diagnosing and
# repairing a broken Linux (or Windows) install. Built on the minimal NixOS
# installer, which already brings NetworkManager, sshd and the usual disk
# tools (testdisk, ddrescue, smartctl, cryptsetup, ...; profiles/base.nix).
#
# nix build .#rescue-iso # -> result/iso/fredos-rescue-*.iso
#
# On boot: type `rescue`. It gets you online (nmtui) and starts Claude as root
# with hosts/rescue/rescue-prompt.md as its system-wide CLAUDE.md. Login is
# the usual URL + code dance (phone works); nothing persists across reboots.
#
# Remote mode: sshd runs with Fred's keys + the server's for root, so Claude
# elsewhere can `ssh root@<ip>` in (or you can run `rescue` over ssh).
{ config, lib, pkgs, modulesPath, ... }:
let
serverKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbIdOSPjnJYQ7z7n0mHKo8T8PnAkCyIBCmT4KpsHyy/ fred@FredOS-Mediaserver";
# Any HTTP answer counts (no -f): the API root is a 404.
online = "${pkgs.curl}/bin/curl -sS -m 5 -o /dev/null https://api.anthropic.com 2>/dev/null";
rescue = pkgs.writeShellScriptBin "rescue" ''
# Claude's binary needs AVX (2011+ CPUs); without it, it spins forever
# instead of crashing.
if ! grep -qw avx /proc/cpuinfo; then
echo "This CPU is too old for Claude (no AVX)."
echo "Use remote mode: get online with 'nmtui', run 'menu' for the"
echo "address, then run Claude on another machine and have it ssh in."
exit 1
fi
# Typed right after boot, wired may still be coming up.
${pkgs.networkmanager}/bin/nm-online -q -t 15
if ! ${online}; then
echo "Not online yet. Pick a network (wired should connect by itself)."
sleep 1
${pkgs.networkmanager}/bin/nmtui-connect
fi
if ! ${online}; then
echo "Still offline. Try 'nmtui', then run 'rescue' again."
exit 1
fi
exec sudo -i ${pkgs.claude-code}/bin/claude "$@"
'';
menu = pkgs.writeShellScriptBin "menu" (builtins.readFile ./rescue/menu.sh);
in
{
imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
../settings/locale.nix
../settings/users.nix # only for Fred's ssh keys below
];
nixpkgs.hostPlatform = "x86_64-linux";
nixpkgs.config.allowUnfreePredicate = pkg: lib.getName pkg == "claude-code";
nix.settings.experimental-features = [ "nix-command" "flakes" ];
networking.hostName = "FredOS-Rescue";
image.baseName = lib.mkForce "fredos-rescue-${config.system.nixos.label}";
isoImage.appendToMenuLabel = " - Claude Rescue"; # no apostrophes: GRUB quotes it with ''
users.users.root.openssh.authorizedKeys.keys =
config.users.users.fred.openssh.authorizedKeys.keys ++ [ serverKey ];
# The installer leaves root/nixos with empty passwords; never over ssh.
services.openssh.settings.PasswordAuthentication = false;
services.openssh.settings.KbdInteractiveAuthentication = false;
environment.etc."claude-code/CLAUDE.md".source = ./rescue/rescue-prompt.md;
environment.systemPackages = with pkgs; [
rescue
menu
claude-code
# Chroot into the patient: arch-chroot does the bind mounts for any
# distro; nixos-enter is already on the ISO for NixOS.
arch-install-scripts
mdadm
chntpw # Windows: reset passwords, edit registry hives
lsof
file
tree
ripgrep
git
rsync
wget
tmux
htop
lm_sensors
dmidecode
memtester
];
# The installer's getty text scrolls past under autologin; the menu replaces
# it. Interactive only, so `sudo -i claude` inside `rescue` doesn't print it.
services.getty.helpLine = lib.mkForce "";
environment.loginShellInit = ''
[[ $- == *i* ]] && menu
'';
# Serial too, for headless boxes (tty0 last = it keeps the boot messages).
boot.kernelParams = [ "console=ttyS0,115200" "console=tty0" ];
# Don't import ZFS pools that weren't exported cleanly; rescue asks first.
boot.zfs.forceImportRoot = false;
}