commit db287114ebdd6e6019c4fa1dddd535aa6f62e6ca
parent 44b2954df0749f9e9debb1f13ded1f9186cc0ef6
Author: DAEMON <zer0sec.xp@icloud.com>
Date: Fri, 9 Oct 2026 00:10:27 +0100
feat(pentest): scaffold per-box trees and add htbcast recordings
- Move htbbox to boxes.nix: `new` takes -n/-i/-d/-o (gum prompts when bare), creates recon/enum/creds/loot/exploit/serve/casts and writes box.json
- Render writeup.md from the vault's Templater template via _boxrender.py, falling back to a built-in skeleton, and never overwrite an existing writeup
- Add htbcast (casts.nix) to record asciinema sessions into the current box, with ls/play/txt/gif and a flake check covering record through gif
- Leave only htbtarget and htbtime in htb.nix, and update the README and pentest cheat sheet to match
Commit-Date: 2026-10-09T02:56:58+01:00
Commit-Host: daemonsec@nixos
Diffstat:
16 files changed, 753 insertions(+), 532 deletions(-)
diff --git a/.sops.yaml b/.sops.yaml
@@ -11,7 +11,7 @@
keys:
- &daemonsec age1pu5wcvqqh92xr6ces8zmj4xvs52v7yfm4jsfcxm3z82a32u9n97qesldfv
creation_rules:
- - path_regex: secrets/.*\.(yaml|json|env|ini)$
+ - path_regex: secrets/.* # any file under secrets/, incl. whole-file (binary) secrets like secrets/floorp/user.js
key_groups:
- age:
- *daemonsec
diff --git a/README.md b/README.md
@@ -84,7 +84,9 @@ NixDaemon/
│ ├── c2.nix database.nix osint.nix social.nix (off by default)
│ ├── vpn.nix htbvpn: the HTB tunnel as a systemd template unit
│ ├── time.nix htb-time: conflict-aware clock skew for Kerberos
- │ ├── htb.nix htbtarget / htbbox: the box you are on, shared across terminals
+ │ ├── htb.nix htbtarget / htbtime: the box you are on, shared across terminals
+ │ ├── boxes.nix htbbox: the per-box tree + writeup.md from the vault template
+ │ ├── casts.nix htbcast: asciinema recordings as raw write-up material
│ ├── devshells.nix nix develop #pentest, and the all-category collision check
│ └── update.nix pentest-update: move the _pkgs pins forward, deliberately
└── home/ flake.homeModules.* — the user's home
@@ -125,7 +127,7 @@ NixDaemon/
| GPU | NVIDIA open module, panel on the RTX; Aquamarine told the GPUs by colon-free udev symlinks | modules/hosts/laptop/nvidia.nix |
| Fan | `fan-throttle-guard`: 3.2 GHz floor, EC auto fan, `uniwill` hwmon from a 6.19 driver. `fan` command: status, watch, max/auto with a clamp watchdog | modules/hosts/laptop/fan-*.nix, uniwill-laptop/, modules/home/fan.nix |
| Pentest | 23 toggleable categories (`daemon.pentest.<category>.enable`, every one listed in `configuration.nix`): recon, AD, web, pivoting, cracking, shells, wordlists, payloads, BloodHound, GUI on; DFIR, reversing, wireless, radio (SDR/BT/RFID), hardware (JTAG/flash/CAN), C2, database, cloud, OSINT, social, mobile off. Tools go to `environment.systemPackages`, so `sudo nmap -sS` works. Every category carries a smoke check: `nix flake check` | modules/features/pentest/ |
-| HTB workflow | `htbvpn up/down/list` (systemd template unit), `htbtarget` (shared across terminals via zsh precmd; writes `/etc/hosts` for Kerberos), `htbtime` (clock skew, restores exactly what it changed), `htbbox new <box>`, `payload-serve` (binds the tunnel only, refuses when the VPN is down), `pentest-cheat` | modules/features/pentest/{vpn,time,htb,payloads}.nix |
+| HTB workflow | `htbvpn up/down/list` (systemd template unit), `htbtarget` (shared across terminals via zsh precmd; writes `/etc/hosts` for Kerberos), `htbtime` (clock skew, restores exactly what it changed), `htbbox new -n <box> -i <ip> -d <difficulty> -o <os>` (gum prompts when bare; scaffolds recon/enum/creds/loot/exploit/serve/casts + `box.json` + `writeup.md` rendered from the vault's Templater template), `htbcast` (asciinema v3 session recording → `txt` transcript for an agent, `gif` preview via agg), `payload-serve` (binds the tunnel only, refuses when the VPN is down), `pentest-cheat` | modules/features/pentest/{vpn,time,htb,payloads}.nix |
| Payloads | `$PAYLOADS`: Windows x64/x86, Linux amd64/arm64 and macOS arm64. ligolo-ng and chisel cross-compiled from source; mimikatz (two versions), the potato family, SharpCollection's 102 C# tools and PEASS pinned by hash | modules/features/pentest/payloads.nix, _pkgs/ |
| Nix | flakes, Hyprland cache, `nh` with weekly `clean all --keep 5 --keep-since 14d` | modules/hosts/laptop/nix-settings.nix |
diff --git a/modules/features/pentest/_boxrender.py b/modules/features/pentest/_boxrender.py
@@ -0,0 +1,129 @@
+"""Render a box's writeup.md and box.json. Driven by env vars from htbbox.
+
+The writeup comes from the vault's Templater template when it is reachable, so
+the vault stays the single source of truth for the post's shape. Templater is
+Obsidian-only JS, so the header block is stripped and the <% ... %> tokens it
+would have filled are substituted here. When the vault is not present (another
+machine, a fresh clone) a minimal built-in frontmatter is used instead.
+"""
+import json
+import os
+import re
+import sys
+from datetime import date
+
+name = os.environ["BOX_NAME"]
+slug = os.environ["BOX_SLUG"]
+ip = os.environ.get("BOX_IP", "")
+os_name = os.environ["BOX_OS"] # Windows | Linux | Other
+difficulty = os.environ["BOX_DIFFICULTY"]
+outdir = os.environ["BOX_DIR"]
+template = os.environ.get("BOX_TEMPLATE", "")
+today = date.today().isoformat()
+
+manifest = {
+ "name": name, "slug": slug, "ip": ip, "os": os_name,
+ "difficulty": difficulty, "platform": "HTB-Labs", "created": today,
+}
+with open(os.path.join(outdir, "box.json"), "w") as fh:
+ json.dump(manifest, fh, indent=2)
+ fh.write("\n")
+
+writeup = os.path.join(outdir, "writeup.md")
+if os.path.exists(writeup):
+ print(f"kept existing {writeup}", file=sys.stderr)
+ sys.exit(0)
+
+body = None
+if template and os.path.exists(template):
+ body = open(template).read()
+ # Drop the Templater header: <%* ... -%> (the JS that prompts in Obsidian).
+ body = re.sub(r"^<%\*.*?-%>\n", "", body, count=1, flags=re.S)
+ # Then the tokens that header would have filled.
+ subs = {
+ "<% yaml(machine) %>": json.dumps(name),
+ "<% yaml(slug) %>": json.dumps(slug),
+ "<% yaml(targetOS) %>": json.dumps(os_name),
+ "<% yaml(difficulty) %>": json.dumps(difficulty),
+ '<% tp.date.now("YYYY-MM-DD") %>': today,
+ "<% targetOS %>": os_name,
+ "<% difficulty %>": difficulty,
+ "<% tp.file.cursor() %>": "",
+ }
+ for token, value in subs.items():
+ body = body.replace(token, value)
+ body = body.replace('ip: ""', f"ip: {json.dumps(ip)}")
+ # A leftover <% ... %> means the vault template grew a token this renderer
+ # does not know. Fail loudly rather than publish Templater source.
+ leftover = re.findall(r"<%.*?%>", body, flags=re.S)
+ if leftover:
+ print(f"htbbox: template has tokens this renderer does not handle: "
+ f"{leftover[:3]} -- update _boxrender.py", file=sys.stderr)
+ sys.exit(1)
+
+if body is None:
+ body = f"""---
+title: {json.dumps(name)}
+slug: {json.dumps(slug)}
+type: writeup
+site: daemon-sec
+category: ctf
+platform: HTB-Labs
+machine: {json.dumps(name)}
+target_os: {json.dumps(os_name)}
+difficulty: {json.dumps(difficulty)}
+author: DAEMON
+excerpt: ""
+status: active
+publish_status: draft
+creation_date: {today}
+published_at: ""
+updated_at: ""
+ip: {json.dumps(ip)}
+tools_used: []
+techniques: []
+bannerImage: ""
+tags:
+ - HTB
+ - HTB/Labs
+cssclasses:
+ - editorial
+ - note-banner
+---
+
+```dataviewjs
+await dv.view("00Meta/Views/NoteBanner");
+```
+
+## Explain like I'm new
+
+## Attack path
+
+## Target details
+
+| Field | Value |
+| --- | --- |
+| IP Address | {ip} |
+| Operating system | {os_name} |
+| Difficulty | {difficulty} |
+
+## Reconnaissance
+
+## Enumeration
+
+## Initial access
+
+## Privilege escalation
+
+## Credentials and flags
+
+## Operator notes
+
+## Lessons learned
+
+## References
+"""
+ print("htbbox: vault template not found, used the built-in skeleton", file=sys.stderr)
+
+with open(writeup, "w") as fh:
+ fh.write(body)
diff --git a/modules/features/pentest/boxes.nix b/modules/features/pentest/boxes.nix
@@ -0,0 +1,194 @@
+# modules/features/pentest/boxes.nix — one directory per box, scaffolded.
+#
+# htbbox new -n Sauna -i 10.10.10.175 -d easy -o windows
+# htbbox new prompt for each field (gum)
+# htbbox ls boxes worked, newest first
+# htbbox info [box] the manifest, and what is in the tree
+# htbbox path [box] just the directory, for `cd "$(htbbox path)"`
+#
+# The layout is the CPTS vocabulary, not the HTB website's:
+#
+# box.json the manifest every other tool reads (ip, os, difficulty, slug)
+# writeup.md the post, rendered from the vault's Templater template
+# recon/ nmap, rustscan, masscan, dns
+# enum/ per-service enumeration output
+# creds/ hashes, passwords, tickets
+# loot/ files pulled off the target
+# exploit/ PoCs and anything you wrote to land a shell
+# serve/ files staged to hand TO the target
+# casts/ terminal recordings (casts.nix)
+#
+# `serve/` rather than `payloads/` on purpose: the toolkit already exports a
+# global $PAYLOADS (payloads.nix) holding the cross-built binaries, so a
+# per-box `payloads/` would read as the same thing and is not.
+#
+# The slug is `htb-<name>` to match content/htb/<slug>/ on the website, and it
+# is recorded in box.json rather than derived twice.
+{ lib, ... }:
+{
+ flake.nixosModules.pentest-boxes =
+ { config, pkgs, lib, ... }:
+ let
+ on = config.daemon.pentest.enable;
+
+ render = pkgs.writeText "htb-boxrender.py" (builtins.readFile ./_boxrender.py);
+
+ htbbox = pkgs.writeShellScriptBin "htbbox" ''
+ set -uo pipefail
+ PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.gum pkgs.jq ]}:$PATH
+ STATE="''${XDG_STATE_HOME:-$HOME/.local/state}/htb"
+ mkdir -p "$STATE"
+ ROOT="$HOME/htb"
+ VAULT="''${NETRUNNER_VAULT:-$HOME/git/NetrunnerVault}"
+ TEMPLATE="$VAULT/00Meta/Templates/daemon-sec-htb-post.md"
+
+ # printf, not a heredoc: this whole script is indented inside a Nix
+ # string, and an INDENTED heredoc terminator does not terminate --
+ # `<<-` strips tabs, not spaces. htb.nix hit this before; bash -n at
+ # build time is what catches it.
+ usage() {
+ printf '%s\n' \
+ 'usage:' \
+ ' htbbox new [-n name] [-i ip] [-d easy|medium|hard|insane] [-o windows|linux|other]' \
+ ' htbbox ls' \
+ ' htbbox info [box]' \
+ ' htbbox path [box]'
+ }
+
+ slugify() {
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]\+/-/g' -e 's/^-//' -e 's/-$//'
+ }
+
+ # The box currently being worked, for the commands that take no argument.
+ current() { [ -s "$STATE/box" ] && cat "$STATE/box"; }
+
+ case "''${1:-ls}" in
+ new)
+ shift
+ name="" ip="" difficulty="" target_os=""
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -n|--name) name="''${2:-}"; shift 2 ;;
+ -i|--ip) ip="''${2:-}"; shift 2 ;;
+ -d|--difficulty) difficulty="''${2:-}"; shift 2 ;;
+ -o|--os) target_os="''${2:-}"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) echo "htbbox: unknown option: $1" >&2; usage >&2; exit 2 ;;
+ esac
+ done
+
+ # Anything not given on the command line is prompted for. gum is
+ # used rather than `read` so the choose lists cannot produce an
+ # invalid value in the first place.
+ if [ -z "$name" ]; then
+ name=$(gum input --prompt "machine name > " --placeholder "Sauna") || exit 1
+ fi
+ [ -n "$name" ] || { echo "htbbox: a machine name is required" >&2; exit 2; }
+ if [ -z "$ip" ]; then
+ ip=$(gum input --prompt "target ip > " --placeholder "10.10.10.175") || exit 1
+ fi
+ if [ -z "$difficulty" ]; then
+ difficulty=$(gum choose --header "difficulty" easy medium hard insane) || exit 1
+ fi
+ if [ -z "$target_os" ]; then
+ target_os=$(gum choose --header "operating system" windows linux other) || exit 1
+ fi
+
+ slug_name=$(slugify "$name")
+ [ -n "$slug_name" ] || { echo "htbbox: name has no usable characters: $name" >&2; exit 2; }
+
+ case "$difficulty" in
+ easy|medium|hard|insane) ;;
+ *) echo "htbbox: difficulty must be easy, medium, hard or insane (got: $difficulty)" >&2; exit 2 ;;
+ esac
+ # Capitalised for the template's frontmatter, which the website reads.
+ case "$(printf '%s' "$target_os" | tr '[:upper:]' '[:lower:]')" in
+ windows) target_os="Windows" ;;
+ linux) target_os="Linux" ;;
+ other) target_os="Other" ;;
+ *) echo "htbbox: os must be windows, linux or other (got: $target_os)" >&2; exit 2 ;;
+ esac
+ if [ -n "$ip" ]; then
+ case "$ip" in
+ *[!0-9.]*) echo "htbbox: not an IPv4 address: $ip" >&2; exit 2 ;;
+ esac
+ fi
+
+ d="$ROOT/$slug_name"
+ mkdir -p "$d"/{recon,enum,creds,loot,exploit,serve,casts}
+
+ BOX_NAME="$name" BOX_SLUG="htb-$slug_name" BOX_IP="$ip" \
+ BOX_OS="$target_os" BOX_DIFFICULTY="$difficulty" BOX_DIR="$d" \
+ BOX_TEMPLATE="$TEMPLATE" \
+ ${pkgs.python3}/bin/python3 ${render} || exit 1
+
+ printf '%s\n' "$slug_name" > "$STATE/box"
+ # Set $TARGET too, so the whole toolkit points at this box at once.
+ # htb.nix owns htbtarget; reached through the profile, as the rest
+ # of the toolkit reaches root-side helpers.
+ if [ -n "$ip" ] && [ -x /run/current-system/sw/bin/htbtarget ]; then
+ /run/current-system/sw/bin/htbtarget "$ip" >/dev/null || true
+ fi
+ echo "$d"
+ ;;
+
+ ls)
+ [ -d "$ROOT" ] || { echo "no boxes yet — htbbox new"; exit 0; }
+ # Built up and printed ONCE, then exit 0 explicitly. A reader like
+ # `htbbox ls | grep -q foo` closes the pipe on its first match, and
+ # every later write then fails with EPIPE and takes the exit status
+ # with it -- which is exactly how the VM test caught this.
+ out=""
+ for j in $(ls -1dt "$ROOT"/*/ 2>/dev/null); do
+ b=''${j%/}; b=''${b##*/}
+ if [ -s "$j/box.json" ]; then
+ out="$out$(printf '%-20s %-15s %-7s %s' "$b" \
+ "$(jq -r '.ip // "-"' "$j/box.json")" \
+ "$(jq -r '.difficulty // "-"' "$j/box.json")" \
+ "$(jq -r '.os // "-"' "$j/box.json")")
+"
+ else
+ out="$out$(printf '%-20s %s' "$b" "(no manifest)")
+"
+ fi
+ done
+ [ -n "$out" ] || out="no boxes yet — htbbox new
+"
+ printf '%s' "$out" || true
+ exit 0 ;;
+
+ info)
+ b="''${2:-$(current)}"
+ [ -n "$b" ] || { echo "htbbox: no box given and none current" >&2; exit 2; }
+ d="$ROOT/$b"
+ [ -d "$d" ] || { echo "htbbox: no such box: $b" >&2; exit 2; }
+ out=""
+ [ -s "$d/box.json" ] && out="$(jq . "$d/box.json")
+"
+ for sub in recon enum creds loot exploit serve casts; do
+ n=$(ls -1A "$d/$sub" 2>/dev/null | wc -l)
+ out="$out$(printf ' %-8s %s' "$sub" "$n")
+"
+ done
+ # One write, then exit 0 -- see the note in `ls`.
+ printf '%s' "$out" || true
+ exit 0 ;;
+
+ path)
+ b="''${2:-$(current)}"
+ [ -n "$b" ] || { echo "htbbox: no box given and none current" >&2; exit 2; }
+ [ -d "$ROOT/$b" ] || { echo "htbbox: no such box: $b" >&2; exit 2; }
+ echo "$ROOT/$b"
+ ;;
+
+ -h|--help) usage ;;
+ *) usage >&2; exit 2 ;;
+ esac
+ '';
+ in
+ {
+ config = lib.mkIf on {
+ environment.systemPackages = [ htbbox ];
+ };
+ };
+}
diff --git a/modules/features/pentest/casts.nix b/modules/features/pentest/casts.nix
@@ -0,0 +1,210 @@
+# modules/features/pentest/casts.nix — record the terminal, as raw material.
+#
+# htbcast record into the current box, auto-named
+# htbcast -n foothold name it yourself
+# htbcast -b sauna -n privesc record into a box that is not the current one
+# htbcast ls [box] recordings, newest first, with durations
+# htbcast play <name> replay it in the terminal
+# htbcast txt <name> plain-text transcript -> casts/<name>.txt
+# htbcast gif <name> animated GIF -> casts/<name>.gif
+#
+# Several at once is the normal case: one terminal running a scan, another
+# working the shell. Each gets its own cast, so the default name carries a
+# counter and a timestamp rather than needing a flag.
+#
+# Why asciinema and not a screen recorder: a .cast is JSON lines of
+# {time, "o", bytes}, so the whole session is machine-readable. That is the
+# point -- these are the input an agent reads to write the write-up, and the
+# `txt` form is what you paste into a prompt.
+#
+# IMPORTANT, and the reason `gif` is not the deliverable: the website's
+# terminal GIFs are generated from a spec, never from a recording
+# (~/git/daemon-sec/AGENTS.md, "ASCII terminal clips"), because a real session
+# carries typos, dead waits and a scrollback that disagrees with the prose.
+# So `htbcast gif` is for previewing and for judging what to cut; the published
+# clip is still written as script/clips/specs/<name>.json.
+#
+# Version note: nixpkgs' asciinema is 3.x and writes asciicast v3. The renderer
+# is `asciinema-agg` (bin/agg), NOT `pkgs.agg`, which is AntiGrain Geometry, a
+# C++ graphics library with no agg binary at all. agg 1.9 does read v3 -- this
+# was checked by recording a cast and rendering it, not assumed.
+{ lib, ... }:
+let
+ # Hoisted out of the module so perSystem.checks can build the same script the
+ # system installs. A check against a different derivation is not a check.
+ mkHtbcast = pkgs: pkgs.writeShellScriptBin "htbcast" ''
+ set -uo pipefail
+ PATH=${lib.makeBinPath [
+ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.jq
+ pkgs.asciinema pkgs.asciinema-agg
+ ]}:$PATH
+ STATE="''${XDG_STATE_HOME:-$HOME/.local/state}/htb"
+ ROOT="$HOME/htb"
+
+ # printf, not a heredoc -- see the note in boxes.nix: an indented
+ # terminator inside a Nix string never terminates.
+ usage() {
+ printf '%s\n' \
+ 'usage:' \
+ ' htbcast [-b box] [-n name] start recording' \
+ ' htbcast ls [box] list recordings' \
+ ' htbcast play <name> [-b box] replay' \
+ ' htbcast txt <name> [-b box] plain-text transcript' \
+ ' htbcast gif <name> [-b box] animated GIF (preview only, see header)'
+ }
+
+ current() { [ -s "$STATE/box" ] && cat "$STATE/box"; }
+
+ # Resolve the box directory, or fail with the command that fixes it.
+ boxdir() {
+ local b="$1"
+ [ -n "$b" ] || b=$(current)
+ if [ -z "$b" ]; then
+ echo "htbcast: no box given and none current — run 'htbbox new' first" >&2
+ return 2
+ fi
+ case "$b" in *[!A-Za-z0-9_.-]*|.*) echo "htbcast: bad box name: $b" >&2; return 2 ;; esac
+ if [ ! -d "$ROOT/$b" ]; then
+ echo "htbcast: no such box: $b (htbbox ls)" >&2
+ return 2
+ fi
+ mkdir -p "$ROOT/$b/casts"
+ printf '%s\n' "$ROOT/$b/casts"
+ }
+
+ sub="''${1:-rec}"
+ case "$sub" in ls|play|txt|gif) shift ;; rec) ;; -h|--help) usage; exit 0 ;; *) sub=rec ;; esac
+
+ box="" name=""
+ # `play`/`txt`/`gif` take the name positionally; everything else by flag.
+ case "$sub" in
+ play|txt|gif) name="''${1:-}"; [ "$#" -gt 0 ] && shift ;;
+ ls) case "''${1:-}" in -*|"") ;; *) box="$1"; shift ;; esac ;;
+ esac
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -b|--box) box="''${2:-}"; shift 2 ;;
+ -n|--name) name="''${2:-}"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) echo "htbcast: unknown option: $1" >&2; usage >&2; exit 2 ;;
+ esac
+ done
+
+ dir=$(boxdir "$box") || exit $?
+
+ case "$sub" in
+ rec)
+ if [ -z "$name" ]; then
+ # Counter + time, so two terminals never collide and the order is
+ # obvious later. date alone is not enough: two shells started in
+ # the same second would overwrite each other.
+ n=$(( $(ls -1 "$dir"/*.cast 2>/dev/null | wc -l) + 1 ))
+ name=$(printf '%02d-%s' "$n" "$(date +%H%M%S)")
+ fi
+ case "$name" in *[!A-Za-z0-9_.-]*|.*) echo "htbcast: bad name: $name" >&2; exit 2 ;; esac
+ out="$dir/$name.cast"
+ [ -e "$out" ] && { echo "htbcast: $out exists — pick another -n" >&2; exit 2; }
+ echo "htbcast: recording to $out — exit the shell (or ctrl-d) to stop"
+ # --title so `ls` and the website spec have something to read back.
+ exec asciinema rec --title "$(basename "$(dirname "$dir")") $name" "$out"
+ ;;
+
+ ls)
+ # One write then exit 0: `htbcast ls | grep -q x` closes the pipe on
+ # its first match and EPIPE would otherwise set a failing status.
+ out=""
+ for c in $(ls -1t "$dir"/*.cast 2>/dev/null); do
+ secs=$(jq -rs 'map(select(type=="array") | .[0]) | if length > 0 then (max | floor | tostring) + "s" else "?" end' "$c" 2>/dev/null || echo "?")
+ out="$out$(printf ' %-28s %-6s %s' "$(basename "$c" .cast)" "$secs" "$(du -h "$c" | cut -f1)")
+"
+ done
+ [ -n "$out" ] || out=" no recordings yet — htbcast
+"
+ printf '%s' "$out" || true
+ exit 0 ;;
+
+ play|txt|gif)
+ [ -n "$name" ] || { echo "htbcast: $sub needs a recording name (htbcast ls)" >&2; exit 2; }
+ c="$dir/$name.cast"
+ [ -s "$c" ] || { echo "htbcast: no such recording: $c" >&2; exit 2; }
+ case "$sub" in
+ play) exec asciinema play "$c" ;;
+ txt) asciinema convert --output-format txt "$c" "$dir/$name.txt" && echo "$dir/$name.txt" ;;
+ gif) agg "$c" "$dir/$name.gif" && echo "$dir/$name.gif" ;;
+ esac
+ ;;
+ esac
+ '';
+in
+{
+ flake.nixosModules.pentest-casts =
+ { config, pkgs, lib, ... }:
+ {
+ config = lib.mkIf config.daemon.pentest.enable {
+ environment.systemPackages = [
+ (mkHtbcast pkgs)
+ pkgs.asciinema
+ pkgs.asciinema-agg
+ ];
+ };
+ };
+
+ # Exercises the whole chain on a real recording: record -> ls -> txt -> gif.
+ # The gif step is the one that matters most: nixpkgs' asciinema is 3.x and
+ # writes asciicast v3, and agg is 1.9. If a future bump breaks that pairing
+ # this check goes red instead of `htbcast gif` failing mid-write-up.
+ perSystem =
+ { pkgs, ... }:
+ {
+ checks.pentest-casts = pkgs.runCommand "pentest-casts-check"
+ {
+ nativeBuildInputs = [ (mkHtbcast pkgs) pkgs.asciinema pkgs.asciinema-agg pkgs.jq ];
+ # agg rasterises text, so it needs a font to exist. The build sandbox
+ # has no fontconfig at all ("Error: no faces matching font family
+ # options"), while the real system does -- so the font is pinned here
+ # rather than in htbcast, which should use whatever the host has.
+ # DejaVu Sans Mono is in agg's default family list.
+ FONTCONFIG_FILE = pkgs.makeFontsConf { fontDirectories = [ pkgs.dejavu_fonts ]; };
+ }
+ ''
+ export HOME=$(mktemp -d)
+ box="$HOME/htb/sauna"
+ mkdir -p "$box/casts"
+
+ # A recording made the way htbcast makes one. --command works without
+ # an interactive tty, which is why this can run in a derivation.
+ asciinema rec --command 'printf "whoami\nnt authority\\system\n"' \
+ "$box/casts/foothold.cast" >/dev/null 2>&1
+ test -s "$box/casts/foothold.cast" || { echo "no cast produced"; exit 1; }
+ grep -q '"version":3' "$box/casts/foothold.cast" \
+ || { echo "expected an asciicast v3 header"; exit 1; }
+
+ # ls must name it, and must not be killed by a reader closing the pipe.
+ htbcast ls -b sauna | grep -q foothold || { echo "ls did not list the cast"; exit 1; }
+ htbcast ls -b sauna | head -1 >/dev/null || { echo "ls died on EPIPE"; exit 1; }
+
+ # txt: the transcript an agent is handed.
+ htbcast txt foothold -b sauna >/dev/null
+ grep -q 'nt authority' "$box/casts/foothold.txt" \
+ || { echo "transcript missing the session output"; exit 1; }
+
+ # gif: asciinema 3 cast through agg 1.9.
+ htbcast gif foothold -b sauna >/dev/null
+ test -s "$box/casts/foothold.gif" || { echo "agg produced no gif"; exit 1; }
+ head -c6 "$box/casts/foothold.gif" | grep -q GIF89a \
+ || { echo "output is not a gif"; exit 1; }
+
+ # A box that does not exist must fail, and say how to list them.
+ # Captured rather than piped: the stdenv builder runs with pipefail,
+ # so htbcast's intended non-zero exit would fail the pipeline even
+ # when grep matches.
+ msg=$(htbcast ls -b nosuchbox 2>&1 || true)
+ printf '%s' "$msg" | grep -q 'htbbox ls' \
+ || { echo "unknown box should point at htbbox ls, got: $msg"; exit 1; }
+ htbcast ls -b nosuchbox >/dev/null 2>&1 \
+ && { echo "unknown box must exit non-zero"; exit 1; } || true
+
+ echo "record -> ls -> txt -> gif all pass" > $out
+ '';
+ };
+}
diff --git a/modules/features/pentest/default.nix b/modules/features/pentest/default.nix
@@ -30,7 +30,9 @@
pentest-bloodhound # BloodHound CE + neo4j + postgresql (manual start)
pentest-vpn # htbvpn: the HTB tunnel as a systemd template unit
pentest-time # htb-time: conflict-aware clock skew for Kerberos
- pentest-htb # htbtarget/htbtime/htb: the box you are on
+ pentest-htb # htbtarget/htbtime: the box you are on
+ pentest-boxes # htbbox: the per-box tree and its writeup.md
+ pentest-casts # htbcast: terminal recordings as raw write-up material
pentest-payloads # $PAYLOADS and payload-serve (multi-arch, cross-built)
pentest-update # pentest-update: move the _pkgs pins forward
pentest-gui # burp, zap, ghidra, wireshark (desktop entries)
diff --git a/modules/features/pentest/htb.nix b/modules/features/pentest/htb.nix
@@ -5,8 +5,10 @@
# htbtarget print it
# htbtarget clear forget it, and clear /etc/hosts
# htbtime [host] clock-skew helper (defaults to $TARGET)
-# htbbox new <box> [ip] scaffold ~/htb/<box> and set the target
-# htbbox ls boxes worked, newest first
+#
+# htbbox moved to boxes.nix, which scaffolds the whole per-box tree and renders
+# writeup.md from the vault template. It still writes $STATE/box and calls
+# htbtarget, so the two halves stay in step.
#
# It is `htbbox`, not `htb`, on purpose: the dotfiles' pentesting.zsh already
# defines an `htb()` shell function, and a zsh function always beats a command
@@ -208,64 +210,10 @@
exec ${sudo} -n /run/current-system/sw/bin/htb-time "$host"
'';
- # A file rather than a heredoc inside the script: an indented heredoc
- # terminator does not terminate (<<- strips tabs, not spaces), and
- # writeShellScriptBin's bash -n caught exactly that.
- notesTemplate = pkgs.writeText "htb-notes-template.md" ''
- # @BOX@
-
- - target: @TARGET@
- - names:
- - started: @DATE@
-
- ## ports
-
- ## foothold
-
- ## credentials
-
- | user | secret | where it works |
- |------|--------|----------------|
-
- ## escalation
-
- ## loot
- '';
-
- htbbox = pkgs.writeShellScriptBin "htbbox" ''
- set -uo pipefail
- PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.gnused ]}:$PATH
- ${stateSh}
- ROOT="$HOME/htb"
-
- case "''${1:-ls}" in
- new)
- box="''${2:-}"
- [ -n "$box" ] || { echo "usage: htbbox new <box> [ip]" >&2; exit 2; }
- case "$box" in *[!A-Za-z0-9_.-]*|.*|"") echo "htbbox: bad box name: $box" >&2; exit 2 ;; esac
- d="$ROOT/$box"
- mkdir -p "$d"/{nmap,loot,creds,www,exploit}
- if [ ! -e "$d/notes.md" ]; then
- ${pkgs.gnused}/bin/sed \
- -e "s|@BOX@|$box|" \
- -e "s|@TARGET@|''${3:-}|" \
- -e "s|@DATE@|$(date -I)|" \
- ${notesTemplate} > "$d/notes.md"
- fi
- printf '%s\n' "$box" > "$STATE/box"
- [ -n "''${3:-}" ] && ${lib.getExe htbtarget} "$3" >/dev/null
- echo "$d" ;;
- ls)
- [ -d "$ROOT" ] || { echo "no boxes yet — htbbox new <box>"; exit 0; }
- ls -1dt "$ROOT"/*/ 2>/dev/null | ${pkgs.gnused}/bin/sed "s|$ROOT/||;s|/$||" || echo "no boxes yet" ;;
- -h|--help) echo "usage: htbbox new <box> [ip] | ls" ;;
- *) echo "usage: htbbox new <box> [ip] | ls" >&2; exit 2 ;;
- esac
- '';
in
{
config = lib.mkIf on {
- environment.systemPackages = [ htbtarget htbtime htbbox htb-hosts ];
+ environment.systemPackages = [ htbtarget htbtime htb-hosts ];
# $TARGET in every terminal.
#
@@ -334,6 +282,7 @@
imports = [
self.nixosModules.pentest-options
self.nixosModules.pentest-htb
+ self.nixosModules.pentest-boxes # htbbox, asserted below
];
daemon.pentest.enable = true;
# The real host has zsh as the login shell; /etc/zshrc (where the
@@ -427,14 +376,46 @@
# htbtime with no target must name the command that sets one.
machine.fail(as_user("htbtime") + " 2>&1 | grep -q htbtarget")
- # Engagement scaffolding.
- out = machine.succeed(as_user("htbbox new escape 10.10.11.202")).strip()
- assert out.endswith("/htb/escape"), out
- for sub in ["nmap", "loot", "creds", "www", "exploit"]:
- machine.succeed(f"test -d /home/daemonsec/htb/escape/{sub}")
- machine.succeed("grep -q '^# escape' /home/daemonsec/htb/escape/notes.md")
+ # Engagement scaffolding (boxes.nix). Every field is passed, so gum
+ # never prompts -- a prompt in a VM test would hang until timeout.
+ out = machine.succeed(as_user(
+ "htbbox new -n EscapeTwo -i 10.10.11.202 -d medium -o windows"
+ )).strip()
+ assert out.endswith("/htb/escapetwo"), out
+ for sub in ["recon", "enum", "creds", "loot", "exploit", "serve", "casts"]:
+ machine.succeed(f"test -d /home/daemonsec/htb/escapetwo/{sub}")
+
+ # The manifest every other tool reads, and the slug the website uses.
+ machine.succeed(
+ "grep -q '\"slug\": \"htb-escapetwo\"' /home/daemonsec/htb/escapetwo/box.json"
+ )
+ machine.succeed(
+ "grep -q '\"difficulty\": \"medium\"' /home/daemonsec/htb/escapetwo/box.json"
+ )
+ # Frontmatter the importer needs, from the built-in skeleton (no
+ # vault in the VM), and no un-substituted Templater tokens anywhere.
+ machine.succeed("grep -q '^target_os: \"Windows\"' /home/daemonsec/htb/escapetwo/writeup.md")
+ machine.succeed("grep -q '^ip: \"10.10.11.202\"' /home/daemonsec/htb/escapetwo/writeup.md")
+ machine.fail("grep -q '<%' /home/daemonsec/htb/escapetwo/writeup.md")
+
+ # Scaffolding a box points the whole toolkit at it.
machine.succeed(as_user("htbtarget") + " | grep -q 10.10.11.202")
- machine.succeed(as_user("htbbox ls") + " | grep -q escape")
+ machine.succeed(as_user("htbbox ls") + " | grep -q escapetwo")
+ machine.succeed(as_user("htbbox path") + " | grep -q /htb/escapetwo")
+ machine.succeed(as_user("htbbox info") + " | grep -q EscapeTwo")
+
+ # Bad input is refused rather than written into the manifest.
+ machine.fail(as_user("htbbox new -n Nope -d impossible -o linux -i 10.0.0.1"))
+ machine.fail(as_user("htbbox new -n Nope -d easy -o plan9 -i 10.0.0.1"))
+ machine.fail(as_user("htbbox new -n Nope -d easy -o linux -i notanip"))
+ machine.fail("test -d /home/daemonsec/htb/nope")
+
+ # Rerunning must not clobber a writeup already being written.
+ machine.succeed("echo 'MY PROSE' >> /home/daemonsec/htb/escapetwo/writeup.md")
+ machine.succeed(as_user(
+ "htbbox new -n EscapeTwo -i 10.10.11.202 -d medium -o windows"
+ ))
+ machine.succeed("grep -q 'MY PROSE' /home/daemonsec/htb/escapetwo/writeup.md")
'';
};
};
diff --git a/modules/home/cheats/nix.md b/modules/home/cheats/nix.md
@@ -254,6 +254,7 @@ git add modules/home/new.nix # make nix see a new file (modified tracked f
nh os build # or switch
jj status # jj snapshots the working copy
jj describe -m "what changed" && jj new # seal it (the vault ritual) — or plain: git add -A && git commit
+jjmsg -c # or let the message write itself from the diff (jjmsg: print · -d describe · -e edit · -c commit)
jj log # history
```
diff --git a/modules/home/cheats/pentest.md b/modules/home/cheats/pentest.md
@@ -20,8 +20,47 @@ switched with `daemon.pentest.<category>.enable` in
...and write /etc/hosts (Kerberos needs names)
htbtarget show it htbtarget clear forget it
- htbbox new escape 10.10.11.202 ~/htb/escape/{nmap,loot,creds,www,exploit}
- htbbox ls boxes, newest first
+ htbbox new prompt for name / ip / difficulty / os
+ htbbox new -n EscapeTwo -i 10.10.11.202 -d medium -o windows
+ htbbox ls boxes, newest first, with ip/difficulty/os
+ htbbox info [box] the manifest, and how full each dir is
+ cd "$(htbbox path)" jump to the current box
+
+ ~/htb/<box>/ holds:
+
+ box.json the manifest every other tool reads (ip, os, difficulty, slug)
+ writeup.md rendered from the vault's daemon-sec-htb-post template
+ recon/ nmap, rustscan, masscan, dns
+ enum/ per-service output creds/ hashes, passwords, tickets
+ loot/ files off the target exploit/ PoCs and what you wrote
+ serve/ files staged FOR the target (not $PAYLOADS, which is global)
+ casts/ terminal recordings
+
+ `htbbox new` also sets $TARGET, so the toolkit points at the box at once.
+ Re-running it keeps an existing writeup.md -- it never clobbers your prose.
+
+## rec — record the session as write-up material
+
+ htbcast record into the current box, auto-named
+ htbcast -n foothold name it
+ htbcast -b sauna -n privesc a box that is not the current one
+ htbcast ls [box] recordings, newest first, with durations
+ htbcast play foothold replay in the terminal
+ htbcast txt foothold plain-text transcript -> casts/foothold.txt
+ htbcast gif foothold animated GIF (preview only, see below)
+
+ Exit the shell (or ctrl-d) to stop. Several at once is the normal case -- one
+ terminal scanning, another on the shell -- so the default name carries a
+ counter and a timestamp and never collides.
+
+ `txt` is the one to paste into Claude or Codex: a .cast is JSON lines of
+ {time, "o", bytes}, so the whole session is machine-readable.
+
+ Caveat: the website's terminal GIFs are written as specs, never recorded
+ (~/git/daemon-sec/AGENTS.md, "ASCII terminal clips") -- a raw session carries
+ typos, dead waits and a scrollback that disagrees with the prose. So
+ `htbcast gif` is for judging what to cut; the published clip is still
+ script/clips/specs/<name>.json.
It is `htbbox`, not `htb`: your dotfiles already define an `htb()` function and
a zsh function always wins over a command on PATH. Your own `htb-newbox` does
diff --git a/modules/home/default.nix b/modules/home/default.nix
@@ -17,6 +17,7 @@
dotfiles # every dotfile from ~/git/daemon-sec-dotfiles, as out-of-store symlinks
cheats # nix-cheat: the rebuild / nh / flake card
sops # sops-nix for the user (same secrets file, age key in ~/.config/sops/age)
+ passage # `passage`: an age-keyed password store for the logins you type
neovim # nvf: Neovim with a small, Nix-built plugin set
prompt # starship prompt and fastfetch card, Rosé Pine, NixOS logo
fan # `fan`: status/watch without root, max/auto with a clamp watchdog
diff --git a/modules/home/floorp.nix b/modules/home/floorp.nix
@@ -1,33 +1,25 @@
# modules/home/floorp.nix — Floorp as the main browser, themed with ShyFox.
#
-# Why this module writes the profile by hand instead of using
-# `programs.floorp.profiles.*`: home-manager builds its Floorp module from
-# mkFirefoxModule with `configPath = ".floorp"`, which was Floorp 11's
-# directory. Floorp 12 keeps its profiles in ~/.config/floorp (this machine has
-# no ~/.floorp at all). Anything set through `programs.floorp.profiles.*`
-# therefore lands where Floorp never looks and is ignored without an error, so
-# `programs.floorp.enable` below is used for the package only and the profile
-# is written through xdg.configFile.
+# The profile is written by hand under ~/.config/floorp (Floorp 12's location)
+# rather than through `programs.floorp.profiles.*`: home-manager's Floorp module
+# still targets ~/.floorp (Floorp 11), so anything set there is ignored without
+# an error. `programs.floorp.enable` below is used for the package only.
#
-# ShyFox could not go through `profiles.<name>.userChrome` in any case: that
-# option takes one string, and the theme is a directory tree (ShyFox/, icons/)
-# that has to sit at <profile>/chrome/.
+# The private parts are sops secrets, not Nix (see .sops.yaml):
+# secrets/floorp/user.js every pref, placed at <profile>/user.js by
+# sops-nix at login and on every switch
+# secrets/floorp/bookmarks.html the bookmarks export Floorp imports once, on a
+# fresh profile (browser.bookmarks.file in user.js)
+# Edit either with `TMPDIR=/dev/shm sops secrets/floorp/<file>`, then
+# `nh os switch` and restart Floorp, which reads user.js only at startup.
+# Saved passwords, cookies, history and sessions are written by Floorp into the
+# profile and are never in this repo.
#
-# The theme is ShyFox 3.8.1 as modernised in gitlab.com/DAEMON-404/ShyFox (synced
-# at 6b69ed8, which fixes Gecko 157 — Floorp 12.19 — hiding the whole toolbox),
-# with Rosé Pine and DM Mono applied on top (_shyfox/ShyFox/shy-rosepine.css,
-# shy-custom.css, content/shy-sidebery-daemon.css — the only files here that are
-# not the fork's; userChrome.css/userContent.css add their imports). To pull a
-# newer fork: rsync its chrome/ over _shyfox/ excluding those two import files.
-# It is vendored into
-# _shyfox/ so this repo pins it, rather than read out of
-# ~/git/daemon-sec-dotfiles, which may not be cloned. `/_` keeps the asset tree
-# out of import-tree's reach (see flake.nix).
-#
-# Nothing is destroyed: the previous profile (1il5n65w.default — the cookies,
-# logins and Proton Pass from before) stays on disk and stays listed in
-# profiles.ini, so Floorp's profile manager can still reach it. Only which
-# profile is *default* changes.
+# The theme is ShyFox, vendored from the gitlab.com/DAEMON-404/ShyFox fork
+# (synced at 6b69ed8, which fixes Gecko 157) into _shyfox/ and linked as
+# <profile>/chrome. To pull a newer fork: rsync its chrome/ over _shyfox/
+# excluding userChrome.css and userContent.css. `/_` keeps the asset tree out of
+# import-tree's reach (see flake.nix).
{ ... }:
{
flake.homeModules.floorp =
@@ -37,104 +29,11 @@
profileDir = "floorp/${profile}";
previousProfile = "1il5n65w.default"; # kept, not default
- ##### Floorp's own design settings ##########################################
- # Floorp injects chrome CSS of its own according to `userInterface`:
- # "fluerial", "photon" and "lepton" all restyle the toolbar and fight
- # ShyFox, which targets modern Firefox (Proton). Pinned to "proton", which
- # is also Floorp's default. The rest is carried over unchanged from the old
- # profile. Floorp stores this as one JSON *string* pref, hence the toJSON
- # here and the second encoding in mkUserJs.
- designConfigs = {
- globalConfigs = {
- userInterface = "proton";
- faviconColor = false;
- appliedUserJs = "";
- };
- tabbar = {
- tabbarStyle = "horizontal";
- tabbarPosition = "default";
- multiRowTabBar = {
- maxRowEnabled = false;
- maxRow = 3;
- };
- };
- tab = {
- tabScroll = {
- enabled = false;
- reverse = false;
- wrap = false;
- };
- tabMinHeight = 30;
- tabMinWidth = 76;
- tabPinTitle = false;
- tabDubleClickToClose = false; # Floorp's own spelling
- tabOpenPosition = -1;
- };
- uiCustomization = {
- navbar = {
- position = "top";
- searchBarTop = false;
- };
- display = {
- disableFullscreenNotification = false;
- deleteBrowserBorder = false;
- };
- special = {
- optimizeForTreeStyleTab = false;
- hideForwardBackwardButton = false;
- stgLikeWorkspaces = false;
- };
- multirowTab.newtabInsideEnabled = false;
- bookmarkBar = {
- focusExpand = false;
- position = "top";
- };
- qrCode.disableButton = false;
- };
- };
-
- ##### Bookmarks #############################################################
- # _bookmarks.nix is the Dia bookmark tree ("Profile 2" of the 2026-10-08
- # macOS backup), converted once at authoring time so the 2.1 GB archive is
- # not a build input. Rendered to the Netscape format Firefox imports.
- bookmarks = import ./floorp/_bookmarks.nix;
-
- bookmarksFile =
- let
- esc = lib.escapeXML;
- indent = n: lib.concatStrings (lib.genList (_: " ") n);
- attr = name: value: " ${name}=\"${esc (toString value)}\"";
- addDate = item: lib.optionalString (item ? addDate) (attr "ADD_DATE" item.addDate);
- toolbar = item: lib.optionalString (item.toolbar or false) (attr "PERSONAL_TOOLBAR_FOLDER" "true");
-
- itemToHTML =
- depth: item:
- if item ? url then
- "${indent depth}<DT><A HREF=\"${esc item.url}\"${addDate item}>${esc item.name}</A>"
- else
- lib.concatStringsSep "\n" [
- "${indent depth}<DT><H3${addDate item}${toolbar item}>${esc item.name}</H3>"
- "${indent depth}<DL><p>"
- (lib.concatStringsSep "\n" (map (itemToHTML (depth + 1)) item.bookmarks))
- "${indent depth}</DL><p>"
- ];
- in
- pkgs.writeText "floorp-bookmarks.html" ''
- <!DOCTYPE NETSCAPE-Bookmark-file-1>
- <!-- Generated by modules/home/floorp.nix from Dia's bookmarks. DO NOT EDIT. -->
- <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
- <TITLE>Bookmarks</TITLE>
- <H1>Bookmarks Menu</H1>
- <DL><p>
- ${lib.concatStringsSep "\n" (map (itemToHTML 1) bookmarks)}
- </DL>
- '';
-
##### Extensions ############################################################
# ShyFox needs both of these: Sidebery provides the sidebar the theme is
# built around, and Userchrome Toggle Extended (by Naezr, who writes ShyFox)
# binds the toggle. Each file has to be named for the add-on's own ID.
- # `extensions.startupScanScopes`/`autoDisableScopes` in the prefs below are
+ # `extensions.startupScanScopes`/`autoDisableScopes` in user.js are
# what make a profile-dropped add-on come up enabled instead of parked as a
# disabled sideload.
extensions = {
@@ -149,101 +48,25 @@
sha256 = "3f5be2684284c0b79aaad0f70872a87f21a9a1329a5eaf8e60090e6f0e6a741d";
};
};
-
- ##### Prefs #################################################################
- # Rendered to user.js, which Floorp re-applies at every startup. The old
- # hand-written file declared `toolkit.legacyUserProfileCustomizations.
- # stylesheets` and the whole floorp.design.configs blob twice, once by hand
- # and once inside ShyFox's installer-managed block; as an attribute set the
- # duplicates collapse. `layout.css.has-selector.enabled` is gone with them:
- # :has() needs no pref on Firefox 140+, as that file's own comment noted.
- prefs = {
- ## Personal
- # The DΛΣMӨП start page on launch, on new windows and on Home. New tabs
- # are left alone: the old profile redirected them with an unsigned XPI
- # built from ~/.config/floorp/startpage-newtab, and that tree does not
- # exist on this machine, so there is nothing to reproduce it from.
- "browser.startup.homepage" = "https://daemon-startpage.vercel.app/";
- "browser.startup.page" = 1;
- # Pick the add-ons above out of <profile>/extensions on every start, and
- # bring them up enabled (14 = 15 minus bit 1).
- "extensions.startupScanScopes" = 1;
- "extensions.autoDisableScopes" = 14;
-
- ## Monospace — matches the system font. DevTools needs no pref: its
- ## --monospace-font-family is "monospace", which fontconfig resolves.
- "font.name.monospace.x-western" = "DMMono Nerd Font";
- "font.name-list.monospace.x-western" = "DMMono Nerd Font, monospace";
- "font.name.monospace.x-unicode" = "DMMono Nerd Font";
- "font.name-list.monospace.x-unicode" = "DMMono Nerd Font, monospace";
-
- ## ShyFox, required
- "toolkit.legacyUserProfileCustomizations.stylesheets" = true; # load chrome/
- "sidebar.revamp" = false; # the new sidebar would replace Sidebery's
- "sidebar.verticalTabs" = false;
- "svg.context-properties.content.enabled" = true; # fill SVG colour
- "widget.gtk.rounded-bottom-corners.enabled" = true;
- "widget.gtk.ignore-bogus-leave-notify" = 1; # fixes Sidebery tab dragging on Linux
-
- ## ShyFox, urlbar behaviour it assumes
- "browser.urlbar.suggest.calculator" = true;
- "browser.urlbar.unitConversion.enabled" = true;
- "browser.urlbar.trimHttps" = true;
- "browser.urlbar.trimURLs" = true;
-
- ## ShyFox options (about:config toggles the theme reads)
- "shyfox.enable.context.menu.icons" = true;
- "shyfox.enable.ext.mono.toolbar.icons" = true;
- "shyfox.enable.ext.mono.context.icons" = true;
- "shyfox.force.native.controls" = true; # Hyprland/GTK window controls, not Adwaita
-
- ## Floorp
- "floorp.design.configs" = builtins.toJSON designConfigs;
-
- ## Floorp extras, all off (2026-10-09). Sidebery is the only sidebar
- ## wanted, so the Panel Sidebar (the icon strip on the right) goes, and
- ## with it the rest of what Floorp bolts on: workspaces, the SSB/PWA
- ## installer, tab stacks, zen mode, the F2 command palette, mouse
- ## gestures, the status bar, Floorp OS and its MCP bridge. Toolbar
- ## buttons Floorp creates without a switch (undo-closed-tab, split view)
- ## are hidden in _shyfox/ShyFox/shy-custom.css instead.
- "floorp.panelSidebar.enabled" = false;
- "floorp.workspaces.enabled" = false;
- "floorp.browser.ssb.enabled" = false;
- "floorp.tabstacks.enabled" = false;
- "floorp.zenmode.enabled" = false;
- "floorp.commandPalette.enabled" = false;
- "floorp.mousegesture.enabled" = false;
- "floorp.splitView.dragToSplitCreate.enabled" = false;
- "floorp.os.enabled" = false;
- "floorp.mcp.enabled" = false;
- "noraneko.statusbar.enable" = false;
-
- ## Firefox-side toolbar clutter that arrived alongside: the built-in
- ## VPN button, the profile-switcher avatar and tab split view.
- "browser.ipProtection.enabled" = false;
- "browser.vpn_promo.enabled" = false;
- "browser.profiles.enabled" = false;
- "browser.tabs.splitView.enabled" = false;
-
- ## Bookmarks — where the HTML import reads from and writes to. The pref
- ## that actually triggers the import is seeded once by the activation
- ## script below, deliberately not from here; see its comment.
- "browser.bookmarks.file" = toString bookmarksFile;
- };
-
- userJs = ''
- // Generated by modules/home/floorp.nix. DO NOT EDIT — edit that module.
- ${lib.concatStringsSep "\n" (
- lib.mapAttrsToList (name: value: "user_pref(\"${name}\", ${builtins.toJSON value});") prefs
- )}
- '';
in
{
# The package only — see the header for why the profile is not built with
# `programs.floorp.profiles.*`.
programs.floorp.enable = true;
+ # Every pref (start page, fonts, ShyFox options, Floorp's design settings and
+ # switched-off extras) as one encrypted file, symlinked to where Floorp reads it.
+ sops.secrets."floorp-user.js" = {
+ sopsFile = ../../secrets/floorp/user.js;
+ format = "binary";
+ path = "${config.xdg.configHome}/${profileDir}/user.js";
+ };
+ # The bookmarks export, at ~/.config/sops-nix/secrets/floorp-bookmarks.html.
+ sops.secrets."floorp-bookmarks.html" = {
+ sopsFile = ../../secrets/floorp/bookmarks.html;
+ format = "binary";
+ };
+
xdg.configFile =
{
# Written here rather than left to Floorp so the themed profile is the
@@ -266,8 +89,6 @@
Version=2
'';
- "${profileDir}/user.js".text = userJs;
-
# The whole theme tree, read-only out of the store. To change the CSS,
# edit modules/home/floorp/_shyfox/ and rebuild. wallpaper.png and
# wallpaper-light.png are the new-tab backgrounds ShyFox's new-tab CSS
@@ -300,27 +121,6 @@
# and duplicate every bookmark. Seeding it into prefs.js instead, only while
# the profile has no places.sqlite yet, imports once on first launch and
# never again.
- # One-time bookmark re-layout (2026-10-09). The first import nested the
- # whole Dia tree under a "Favourites" folder on the toolbar; the tree in
- # _bookmarks.nix is flat now, but places.sqlite already exists and the
- # import never re-runs, so the live database is reshaped once here: the
- # folder's children move onto the Bookmarks Toolbar and the folder goes.
- # Guarded so it only touches the database while Floorp is not running and
- # only while that folder is still there; afterwards it is a no-op and this
- # block can be deleted.
- home.activation.floorpFlattenFavourites = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
- floorpPlaces="${config.xdg.configHome}/${profileDir}/places.sqlite"
- sqlite=${pkgs.sqlite}/bin/sqlite3
- if [ -e "$floorpPlaces" ] && ! ${pkgs.procps}/bin/pgrep -x .floorp-wrapped >/dev/null \
- && [ "$($sqlite "$floorpPlaces" "SELECT COUNT(*) FROM moz_bookmarks WHERE type=2 AND title='Favourites' AND parent=(SELECT id FROM moz_bookmarks WHERE guid='toolbar_____')")" = 1 ]; then
- verboseEcho "Floorp: moving the Favourites folder's bookmarks onto the toolbar"
- if [ -z "''${DRY_RUN:-}" ]; then
- cp -p "$floorpPlaces" "$floorpPlaces.before-flatten"
- $sqlite "$floorpPlaces" < ${./floorp/_flatten-favourites.sql}
- fi
- fi
- '';
-
home.activation.floorpSeedBookmarkImport = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
floorpProfile="${config.xdg.configHome}/${profileDir}"
floorpPrefs="$floorpProfile/prefs.js"
diff --git a/modules/home/floorp/_bookmarks.nix b/modules/home/floorp/_bookmarks.nix
@@ -1,206 +0,0 @@
-# Bookmarks carried over from Dia (Chromium-format 'Bookmarks' JSON,
-# profile "Profile 2" of the 2026-10-08 macOS backup). Generated once at
-# authoring time and committed as data, so the 2.1 GB archive is not a
-# build input. modules/home/floorp.nix renders it to a Netscape
-# bookmarks.html that Floorp imports on the profile's first launch.
-#
-# Shape: { name; url; addDate?; } is a bookmark,
-# { name; addDate?; bookmarks = [ ... ]; } is a folder,
-# toolbar = true marks Firefox's PERSONAL_TOOLBAR_FOLDER.
-[
- {
- name = "Bookmarks Toolbar";
- toolbar = true;
- bookmarks = [
- {
- name = "Annas-archive";
- addDate = 1790646372;
- bookmarks = [
- { name = "Anna’s Archive pk"; url = "https://annas-archive.pk/"; addDate = 1790646372; }
- { name = "Anna’s Archive gl"; url = "https://annas-archive.gl/"; addDate = 1790646372; }
- { name = "Anna’s Archive gd"; url = "https://annas-archive.gd/"; addDate = 1790646372; }
- ];
- }
- {
- name = "Hacking";
- addDate = 1790646372;
- bookmarks = [
- {
- name = "Jobs";
- addDate = 1790646372;
- bookmarks = [
- { name = "Mi5 Jobs"; url = "https://www.wikijob.co.uk/application-advice/internships-graduate-schemes/mi5-graduate-scheme?utm_source=chatgpt.com"; addDate = 1790646372; }
- { name = "MI5"; url = "https://recruitmentservices.applicationtrack.com/vx/lang-en-GB/mobile-0/appcentre-1/brand-5/user-4262240/xf-d3bb7f5dfb9a/candidate"; addDate = 1790646372; }
- { name = "Police Scotland hiring Graduate Programme - Graduate Cyber Security Analyst in Scotland, United Kingdom | LinkedIn"; url = "https://uk.linkedin.com/jobs/view/graduate-programme-graduate-cyber-security-analyst-at-police-scotland-4347769408"; addDate = 1790646372; }
- ];
- }
- { name = "SANS Tools"; url = "https://www.sans.org/tools"; addDate = 1790646372; }
- { name = "Home"; url = "https://learning.oreilly.com/home/"; addDate = 1790646372; }
- { name = "HTB Fluffy Pentest Automation Script - DeepSeek"; url = "https://chat.deepseek.com/a/chat/s/f2c6c169-3c39-442d-9dad-acf67c516b13"; addDate = 1790646372; }
- { name = "ENI LIME -may official - Google Docs"; url = "https://docs.google.com/document/d/1J3h3tsS3eIQ7hpmDeXq6c9Ea7cpN4WuY-xlCk4hWSpg/edit?pli=1&tab=t.0"; addDate = 1790646372; }
- { name = "edclub"; url = "https://www.edclub.com/sportal/program-3/2940.play"; addDate = 1790646372; }
- {
- name = "Blogs";
- addDate = 1790646372;
- bookmarks = [
- { name = "Axura"; url = "https://4xura.com/"; addDate = 1790646372; }
- { name = "HackTricks - HackTricks"; url = "https://book.hacktricks.wiki/en/index.html"; addDate = 1790646372; }
- { name = "Red Team Notes"; url = "https://www.ired.team/"; addDate = 1790646372; }
- ];
- }
- {
- name = "Learning";
- addDate = 1790646372;
- bookmarks = [
- { name = "PayloadsAllTheThings"; url = "https://github.com/swisskyrepo/PayloadsAllTheThings"; addDate = 1790646372; }
- { name = "OffSec"; url = "https://portal.offsec.com/dashboard"; addDate = 1790646372; }
- { name = "pwn.college"; url = "https://pwn.college/"; addDate = 1790646372; }
- { name = "HackTheBox | BreachForums"; url = "https://breachforums.bf/Forum-HackTheBox"; addDate = 1790646372; }
- { name = "RedBlock Market"; url = "https://offsecexams.com/"; addDate = 1790646372; }
- { name = "Python Tools and Scripts w/ UV CheatSheet | 0xdf hacks stuff"; url = "https://0xdf.gitlab.io/cheatsheets/uv#"; addDate = 1790646372; }
- { name = "0xJerry's Lab"; url = "https://0xjerry.jerome.co.in/"; addDate = 1790646372; }
- { name = "Uphack"; url = "https://uphack.io/"; addDate = 1790646372; }
- { name = "Cyberly - Free Cybersecurity & Real-World Technology Tutorials"; url = "https://www.cyberly.org/en/index.html"; addDate = 1790646372; }
- ];
- }
- { name = "Practice"; url = "https://www.keybr.com/"; addDate = 1790646372; }
- { name = "Codédex"; url = "https://www.codedex.io/"; addDate = 1790646372; }
- ];
- }
- {
- name = "High Sea's";
- addDate = 1790646372;
- bookmarks = [
- { name = "MacBB"; url = "https://macbb.org/"; addDate = 1790646372; }
- {
- name = "Streaming";
- addDate = 1790646372;
- bookmarks = [
- { name = "P-Stream"; url = "https://pstream.mov/"; addDate = 1790646372; }
- { name = "Cineby"; url = "https://www.cineby.gd/"; addDate = 1790646372; }
- { name = "BEECH"; url = "https://www.beech.watch/"; addDate = 1790646372; }
- { name = "h!anime"; url = "https://hianime.to/"; addDate = 1790646372; }
- { name = "fmhy"; url = "https://fmhy.net/"; addDate = 1790646372; }
- { name = "XPrime"; url = "https://xprime.tv/"; addDate = 1790646372; }
- { name = "Bingeflix"; url = "https://bingeflix.tv/"; addDate = 1790646372; }
- { name = "SpenFlix"; url = "https://watch.spencerdevs.xyz/"; addDate = 1790646372; }
- { name = "BFM"; url = "https://my.brain.fm/player/QYN78dJrrzs5mD_TJgBcq"; addDate = 1790646372; }
- ];
- }
- { name = "Download Free Games and Softwares for Mac, PC & More | ToxicGame"; url = "https://toxicgame.net/"; addDate = 1790646372; }
- { name = "Patched.to"; url = "https://patched.to/"; addDate = 1790646372; }
- { name = "Cracked.sh - Beyond the Limits"; url = "https://cracked.sh/"; addDate = 1790646372; }
- { name = "DarkForums"; url = "https://darkforums.io/"; addDate = 1790646372; }
- { name = "Active Directory - Certificate ESC Attacks"; url = "https://swisskyrepo.github.io/InternalAllTheThings/active-directory/ad-adcs-esc/"; addDate = 1790646372; }
- { name = "CyberArsenal"; url = "https://cyberarsenal.org/"; addDate = 1790646372; }
- { name = "web4s"; url = "https://web4sync.com/"; addDate = 1790646372; }
- ];
- }
- {
- name = "Shopping";
- addDate = 1790646372;
- bookmarks = [
- {
- name = "Switch";
- addDate = 1790646372;
- bookmarks = [
- { name = "Animal Crossing New Horizons for sale | eBay"; url = "https://www.ebay.co.uk/sch/i.html?_nkw=Animal+Crossing+New+Horizons&_sacat=0&_from=R40&_trksid=p2332490.m570.l1313"; addDate = 1790646372; }
- ];
- }
- {
- name = "Clothing";
- addDate = 1790646372;
- bookmarks = [
- { name = "CITY NOISE RECORDS / T-SHIRTS / PUNK GEAR - WE BUY COLLECTIONS! PDX"; url = "https://citynoiserecords.com/"; addDate = 1790646372; }
- { name = "Seditionaries T-Shirts | Inspired by 70s punk"; url = "https://poisonclothing.com/product-category/mens/seditionaries-shirts/"; addDate = 1790646372; }
- { name = "Sexy Hooligans"; url = "https://sexyhooligans.com/"; addDate = 1790646372; }
- { name = "Tiger of London"; url = "https://www.tigeroflondon.co.uk/"; addDate = 1790646372; }
- ];
- }
- { name = "ADHD Meds"; url = "https://patient-services.co.uk/"; addDate = 1790646372; }
- { name = "mm420"; url = "https://mm420bot.com/"; addDate = 1790646372; }
- ];
- }
- {
- name = "ArchiveSites";
- addDate = 1790646372;
- bookmarks = [
- { name = "YouTube"; url = "https://www.youtube.com/"; addDate = 1790646372; }
- { name = "archive.ph"; url = "https://archive.ph/"; addDate = 1790646372; }
- { name = "FreezePage"; url = "https://www.freezepage.com/"; addDate = 1790646372; }
- { name = "Home | PreserveTube"; url = "https://preservetube.com/"; addDate = 1790646372; }
- {
- name = "Gross Autistic Cunt";
- addDate = 1790646372;
- bookmarks = [
- { name = "Han_9999"; url = "https://kiwifarms.st/threads/anisa-riyadh-jomha-anisajomha-idubbbz-ian-kane-jomha-ian-kane-washburn-anisas-husband-poo-pants-swastika-boy.38107/page-2292#post-21290412"; addDate = 1790646372; }
- ];
- }
- { name = "Ghostarchive"; url = "https://ghostarchive.org/"; addDate = 1790646372; }
- { name = "Wayback Machine"; url = "https://web.archive.org/"; addDate = 1790646372; }
- { name = "Tweet Archive"; url = "https://www.tweetarchive.org/"; addDate = 1790646372; }
- { name = "Webpage archive"; url = "https://archive.is/"; addDate = 1790646372; }
- { name = "Wayback Tweets"; url = "https://waybacktweets.streamlit.app/"; addDate = 1790646372; }
- { name = "Social Media Archiving"; url = "https://secure.archivesocial.com/login"; addDate = 1790646372; }
- { name = "SteamHistory"; url = "https://steamhistory.net/"; addDate = 1790646372; }
- { name = "SAM HYDE TORRENT ARCHIVE"; url = "https://rentry.org/payangel"; addDate = 1790646372; }
- { name = "b"; url = "https://www.instagram.com/bethlivingbalanced/"; addDate = 1790646372; }
- { name = "b"; url = "https://www.facebook.com/profile.php?id=100082126715073&sk=friends_all"; addDate = 1790646372; }
- ];
- }
- { name = "DAEMON://DECK"; url = "http://localhost:8080/"; addDate = 1790646372; }
- { name = "Netrunner // Ops Board"; url = "file:///Users/daemon1/Library/Application%20Support/Claude/local-agent-mode-sessions/d7004bf7-1ca4-41e8-ba6c-d2536157fb69/85283e6f-2181-49fb-a713-3e92c0dd2883/local_41414789-7bad-4aa7-af4e-aa8d6afd21d5/outputs/cpts-dashboard.html"; addDate = 1790646372; }
- { name = "DΛΣMӨП"; url = "https://daemon-sec.xyz/"; addDate = 1790646372; }
- { name = "GitHub"; url = "https://github.com/"; addDate = 1790646372; }
- { name = "YouTube"; url = "https://www.youtube.com/"; addDate = 1790646372; }
- { name = "HackTheBox"; url = "https://account.hackthebox.com/dashboard"; addDate = 1790646372; }
- { name = "PortSwigger"; url = "https://portswigger.net/web-security"; addDate = 1790646372; }
- { name = "GBG"; url = "https://gitbybit.com/"; addDate = 1790646372; }
- { name = "cyber_jobs_dashboard"; url = "file:///Users/daemon1/Library/Application%20Support/Claude/local-agent-mode-sessions/d7004bf7-1ca4-41e8-ba6c-d2536157fb69/85283e6f-2181-49fb-a713-3e92c0dd2883/local_287e3b5b-cbed-41a9-ade2-fa7f9c1aa8de/outputs/cyber_jobs_dashboard.html"; addDate = 1790646372; }
- { name = "Kiwi Farms"; url = "https://kiwifarms.st/"; addDate = 1790646372; }
- { name = "BC-Toolkit"; url = "https://bellingcat.gitbook.io/toolkit"; addDate = 1790646372; }
- { name = "GitTuts"; url = "https://www.gitmastery.me/"; addDate = 1790646372; }
- { name = "Cineby"; url = "https://www.cineby.gd/"; addDate = 1790646372; }
- { name = "Monkeytype"; url = "https://monkeytype.com/"; addDate = 1790646372; }
- { name = "Unicorn Studio"; url = "https://www.unicorn.studio/dashboard"; addDate = 1790646372; }
- { name = "Lenovo Legion Pro 5 16 Inch WQXGA Gaming Laptop (Intel Core i9-14900HX, NVIDIA GeForce RTX 5070, 32 GB RAM, 1 TB SSD, 240 Hz, Wi-Fi 6, Win 11 Home) – Eclipse Black : Amazon.co.uk: Computers & Accessories"; url = "https://www.amazon.co.uk/Lenovo-Legion-Gaming-i9-14900HX-GeForce/dp/B0FQVB3NK4/ref=sr_1_2?dib=eyJ2IjoiMSJ9.UU3mGyAvyi_6xrrq8GlRfLN2ItEee0oOuG9rtUmUeD5iPFMIhH7JrY9Ukk8L16hm0y46btzQPlMJBl5mjl8hX1MEw5OzypaiUgm6XxKJHNUuPF2FqdOw2o4OIWyDfPlcDB-19n9cJqnwXekSynP1o9VjQIf7vK4pv1JAkcZbgF69OoOVnGPaoNN8CKFSXCb8w2atn2Q2RBSBo5oXeAX-kd0vglhr0eW66nKkKyfyXrg.VG8roLW6wJI4mVe7FNuAYTAyt92JfQz_neZToWAXP1g&dib_tag=se&keywords=lenovo%2Blegion&qid=1784132112&sr=8-2&th=1"; addDate = 1790646372; }
- { name = "Flixer - Stream Free Movies & TV Shows Online"; url = "https://flixer.gd/search"; addDate = 1790646372; }
- { name = "ShuttleTV"; url = "https://shuttletv.su/"; addDate = 1790646372; }
- { name = "Store - Cyber Cheats"; url = "https://cyber-cheats.com/"; addDate = 1790646372; }
- { name = "Age of Craft"; url = "https://ageofcraft.com/"; addDate = 1790646372; }
- { name = "✞ (@paaxvobiscum) | TikTok"; url = "https://www.tiktok.com/@paaxvobiscum"; addDate = 1790646372; }
- { name = "Home | The Accept Bitcoin Cash Initiative"; url = "https://acceptbitcoin.cash/#cloud"; addDate = 1790646372; }
- { name = "Most relevant stories - Fimfiction"; url = "https://www.fimfiction.net/stories?q=Human+Crossover+status%3Acomplete&order=relevance"; addDate = 1790646372; }
- {
- name = "FiM";
- addDate = 1790646372;
- bookmarks = [
- { name = "(2) Your Human and You - Prologue: Standing in Your Grave [HiE] - YouTube"; url = "https://www.youtube.com/watch?v=qN34G65dnRI&list=PL964cNVUoujnb40M8Z3zZ7ZIz9lCDuPap"; addDate = 1790646372; }
- { name = "(2) My new life in Equestria Chapter 1 - YouTube"; url = "https://www.youtube.com/watch?v=BZJ_6yC1r1I&list=PLHhCfjGyNGAUjb8bBZ7YVbo9uQ4Qs8dsI"; addDate = 1790646372; }
- { name = "(2) [MLP Fanfic Reading] \"The Last Son of Dublin - Chapter 1\" by PaleNarrator (Slice of Life) - YouTube"; url = "https://www.youtube.com/watch?v=OmREtoj1724"; addDate = 1790646372; }
- { name = "(2) A Man In A Mare's World - Prologue: Escape [Requested] (Fanfic Reading - Anthro/Dark MLP) - YouTube"; url = "https://www.youtube.com/watch?v=LUPxJ9al0jk&list=PLbpyrotvg27wrPKwm3yF9rN49RgEjlGsh"; addDate = 1790646372; }
- { name = "(2) anokoVA - YouTube"; url = "https://www.youtube.com/@atelieranoko"; addDate = 1790646372; }
- { name = "(2) \"I Got You\" (Romance) - YouTube"; url = "https://www.youtube.com/watch?v=Lx0vLWYrw2A"; addDate = 1790646372; }
- { name = "(2) 【MLP RP】🌙🌈 Princess Celestia & Luna Fight Over Your Favourite Time of Day... - YouTube"; url = "https://www.youtube.com/watch?v=IcCuBbFVEb0"; addDate = 1790646372; }
- { name = "【MLP ASMR】💖🌈 Princess Celestia orders for Cuddles NOW !! - YouTube"; url = "https://www.youtube.com/watch?v=Lcdi0HBSoKk"; addDate = 1790646372; }
- { name = "(2) 【MLP RP】☁️💗 Princess Cadance Secretly wants you to Herself... - YouTube"; url = "https://www.youtube.com/watch?v=zaU56AOetJk"; addDate = 1790646372; }
- { name = "(2) Equestrian Human Spirit Chapter 1 Narrated by Zero Eclipse Written by ShadicBro Bron - YouTube"; url = "https://www.youtube.com/watch?v=qP7xTtpmiMA&list=PLI42bMrLRvX7Cpeqhrt2jh6jXPe7U5EXr"; addDate = 1790646372; }
- { name = "(2) 【MLP ASMR】🌙🌈 Celestia's attempt at Holding Luna back... - YouTube"; url = "https://www.youtube.com/watch?v=MTeNXnsvizI"; addDate = 1790646372; }
- { name = "Everfree Guardian [Chapter 2 - Part 1] (Fanfic Reading - Anthro/Dramatic MLP) - YouTube"; url = "https://www.youtube.com/watch?v=lUVzHwV6ydI&list=PLbpyrotvg27wpVJrqzobpweVx61jTlNV1&index=1"; addDate = 1790646372; }
- ];
- }
- { name = "Download subreddit or user data"; url = "https://arctic-shift.photon-reddit.com/download-tool"; addDate = 1790646372; }
- { name = "Communist Hammer and Sickle Flag Patch. Iron On, Velcro or Sew On! Communism - Socialist - Socialism - Etsy Denmark"; url = "https://www.etsy.com/dk-en/listing/1889344878/communist-hammer-and-sickle-flag?ls=s&ga_order=most_relevant&ga_search_type=all&ga_view_type=gallery&ga_search_query=communist+patch&ref=sr_gallery-1-16&content_source=fe2ad396-fa40-42c8-9e87-2a38a06e4021%253ALT4731ad8ffa02b53e1c7c1f7c0799b957c7ad1664&organic_search_click=1&logging_key=fe2ad396-fa40-42c8-9e87-2a38a06e4021%3ALT4731ad8ffa02b53e1c7c1f7c0799b957c7ad1664&variation0=5279782817"; addDate = 1790646372; }
- { name = "Landsknecht armor kit of the XVI century for sale | Steel Mastery"; url = "https://steel-mastery.com/de/landsknecht-plate-armor-xvi-century-662878?srsltid=AfmBOop3oqGIlUgtw7z3QNKtwfZjUbluC8E-2HxEdIkAGA77d9g1C9NW"; addDate = 1790646372; }
- { name = "THE JAILBREAK INDEX"; url = "https://slowlow999.github.io/The_Jailbreak_Index/"; addDate = 1790646372; }
- { name = "JailbreakDB — An indexed catalog of working LLM jailbreak techniques."; url = "https://jailbreakdb.com/"; addDate = 1790646372; }
- { name = "Jailbreaks FYI — Working LLM jailbreak techniques, sourced and dated."; url = "https://jailbreaks.fyi/"; addDate = 1790646372; }
- { name = "Alcor - Canterlot Comics"; url = "https://www.canterlotcomics.com/author/alcor90-366"; addDate = 1790646372; }
- { name = "Dueling Cloak - Etsy Denmark"; url = "https://www.etsy.com/market/dueling_cloak"; addDate = 1790646372; }
- { name = "Create beautiful images of your code"; url = "https://ray.so/#theme=triggerdev&padding=64"; addDate = 1790646372; }
- { name = "The One Ring – Officially Licensed Replicas | Jens Hansen"; url = "https://www.jenshansen.com/collections/the-one-ring-replica?srsltid=AU7gw4UyODWfO01NTdngoj4s_W27-f1E90oIQ7VyTKn4BrbW6bsWcmbE"; addDate = 1790646372; }
- { name = "Huawei Matebook Fold 32GB+1TB Blue"; url = "https://bludiode.com/en/tablets-laptops/huawei-matebook-fold-32gb1tb-blue-28814/?RgExcludeLocationDetection=&SubmitCurrency=1&id_currency=1&srsltid=AU7gw4XAz4tCz3LLJOd4LT9zwB7Y7ea24XA2MZgYmX4MXvq8nfhkNdIpHoE"; addDate = 1790646372; }
- { name = "Cyber Verification Program | Verification Portal"; url = "https://portal.anthropic.com/programs/cvp"; addDate = 1790646372; }
- ];
- }
-]
diff --git a/modules/home/floorp/_flatten-favourites.sql b/modules/home/floorp/_flatten-favourites.sql
@@ -1,24 +0,0 @@
--- Move the children of the "Favourites" folder up onto the Bookmarks Toolbar and drop the folder.
--- Idempotent: does nothing unless Favourites sits directly under toolbar_____.
-BEGIN;
-CREATE TEMP TABLE fav AS
- SELECT id FROM moz_bookmarks
- WHERE type=2 AND title='Favourites'
- AND parent=(SELECT id FROM moz_bookmarks WHERE guid='toolbar_____') LIMIT 1;
-CREATE TEMP TABLE kids AS
- SELECT id, ROW_NUMBER() OVER (ORDER BY position) - 1 AS pos
- FROM moz_bookmarks WHERE parent=(SELECT id FROM fav);
-UPDATE moz_bookmarks SET
- parent=(SELECT id FROM moz_bookmarks WHERE guid='toolbar_____'),
- position=(SELECT pos FROM kids WHERE kids.id=moz_bookmarks.id),
- lastModified=CAST(strftime('%s','now') AS INTEGER)*1000000,
- syncChangeCounter=syncChangeCounter+1
-WHERE id IN (SELECT id FROM kids);
--- shift any other toolbar siblings behind the moved ones
-UPDATE moz_bookmarks SET position=position+(SELECT COUNT(*) FROM kids)
-WHERE parent=(SELECT id FROM moz_bookmarks WHERE guid='toolbar_____')
- AND id NOT IN (SELECT id FROM kids) AND id<>(SELECT id FROM fav);
-DELETE FROM moz_bookmarks WHERE id=(SELECT id FROM fav);
-UPDATE moz_bookmarks SET lastModified=CAST(strftime('%s','now') AS INTEGER)*1000000, syncChangeCounter=syncChangeCounter+1
-WHERE guid='toolbar_____';
-COMMIT;
diff --git a/modules/home/passage.nix b/modules/home/passage.nix
@@ -0,0 +1,64 @@
+# modules/home/passage.nix — `passage`: a password store, keyed by the age key
+# the flake already has.
+#
+# passage init not needed; the recipients come from the environment
+# passage insert web/gitlab
+# passage show web/gitlab passage -c web/gitlab (clipboard, 45s)
+# passage generate web/newsite 32
+# passage ls / passage find gitlab / passage grep token
+# passage git init && passage git remote add ...
+#
+# Why passage and not pass: `pass` is GnuPG, which means the agent, pinentry
+# and a keyring. passage is the same tool reimplemented on age, and this
+# machine already keeps an age key for sops-nix. One key, one backup.
+#
+# Why it reuses ~/.config/sops/age/keys.txt rather than its own identity:
+# that file already decrypts every secret the flake has, so pointing passage
+# at it adds no exposure that is not already there, and -- more importantly --
+# no SECOND 189-byte file whose loss is unrecoverable. The alternative, a
+# dedicated passphrase-protected identity, is in the vault note; it is a real
+# improvement in compartmentalisation and a real increase in what you must
+# back up. Switch by changing PASSAGE_IDENTITIES_FILE below.
+#
+# This is deliberately NOT where machine secrets live. Anything the system or a
+# service needs at activation belongs in secrets/secrets.yaml via sops-nix
+# (modules/home/sops.nix) -- passage needs an interactive shell and your
+# clipboard, which activation has neither of. passage is for the logins you
+# type, sops for the values the machine reads.
+{ ... }:
+{
+ flake.homeModules.passage =
+ { config, pkgs, ... }:
+ let
+ ageKey = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
+ # The public half of that key. Public by definition -- it is already
+ # committed in ~/NixDaemon/.sops.yaml as the only recipient. Kept here as a
+ # literal for the same reason gpg.nix pins a fingerprint: so a wrong key
+ # cannot be picked up silently from a file that happens to be on disk.
+ recipient = "age1pu5wcvqqh92xr6ces8zmj4xvs52v7yfm4jsfcxm3z82a32u9n97qesldfv";
+ in
+ {
+ home.packages = [ pkgs.passage ];
+
+ home.sessionVariables = {
+ # Defaults are ~/.passage/store and ~/.passage/identities; both are set
+ # explicitly so the store can move without surprises.
+ PASSAGE_DIR = "${config.xdg.dataHome}/passage/store";
+ PASSAGE_IDENTITIES_FILE = ageKey;
+
+ # Recipients as an environment variable rather than a .age-recipients
+ # file in the store: it keeps the store a plain directory of .age files
+ # with nothing to commit by accident, and makes the recipient part of the
+ # flake rather than per-checkout state. For per-subdirectory recipients
+ # (sharing one folder with someone else) drop a .age-recipients in that
+ # folder instead -- passage walks up from the entry to find one, and a
+ # file found that way wins over this.
+ PASSAGE_RECIPIENTS = recipient;
+
+ # pass-compatible knobs passage still honours.
+ PASSWORD_STORE_CLIP_TIME = "45"; # seconds before the clipboard is wiped
+ PASSWORD_STORE_GENERATED_LENGTH = "25";
+ };
+ }
+ ;
+}
diff --git a/secrets/floorp/bookmarks.html b/secrets/floorp/bookmarks.html
@@ -0,0 +1,14 @@
+{
+ "data": "ENC[AES256_GCM,data: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,iv:Y7jW1Hsu/TXMtkQTseiJYiAFHWj0E86ZebYkjcP57SE=,tag:CgPsdxfJSHFhmMQjgsVK3w==,type:str]",
+ "sops": {
+ "age": [
+ {
+ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2SnFPZTNveGN2Nk5mNlUz\nQmg0NmJDcXNNYWtDMjVXQmZZdFNTZnpFajMwCmFobUhkTzlEdXJYbTRlMXRiTEdN\nRnJiVU8ycEkvK1JUSmxLTzUzU2NVWWcKLS0tIExOOFBCek1SYkcvL3ZIaVFqWlRx\nTFE0T2NmTG5Bd3FBRXMzck4xYlA0UFEKNx64DUSStHwnWuep7CrgGWCQu1qfpdFu\nTKgEStvEmZQvNABvhGpxSZL/HRQLVHa1BMVw5MCDiBpMlVKOD9wO6g==\n-----END AGE ENCRYPTED FILE-----\n",
+ "recipient": "age1pu5wcvqqh92xr6ces8zmj4xvs52v7yfm4jsfcxm3z82a32u9n97qesldfv"
+ }
+ ],
+ "lastmodified": "2026-10-09T01:33:35Z",
+ "mac": "ENC[AES256_GCM,data:VoqStq0xk9uvAJ6D4U9NqFMpQvktWe7YJgqWSEBKGmUeh/RUGbIKsecqAsmf4uI0lJQvr5qCtE6BPJB+VafUmOmrOca+yQQWc7lBYldHAM/1e/4EgK45rCDhA1A+tlixWzgtZRNIYiXuFDFlYvlybDwS5/W82w1dS4nX9m1Zq/g=,iv:MNI7GwLuA9/Kwy/0txrwXuuFJQ7TVCGsyF8ZBoDPXeM=,tag:5MgZByAItPtgcjK44NJLtw==,type:str]",
+ "version": "3.13.3"
+ }
+}
diff --git a/secrets/floorp/user.js b/secrets/floorp/user.js
@@ -0,0 +1,14 @@
+{
+ "data": "ENC[AES256_GCM,data:oy/i4kBeY4SVpFiSRpicYMrPnR/RpnaOEs3zoLJ27Hun4Vi5pzBGglfS2o3+WDEtovknWidphP5ZdvS6eXXNeEfCALjXAQ22dJ2c2q93kYSzm+QGgxDU+BPf49yH1nMkF/55l5WphnM62V7MzJD9wUoVK8WUM6RlesTaSshji+ibS+JWFI93IIPirD4iEk7fI3Um+BRJjB3xlut5a0oLKfPMNZ6xVB6WsPcxUrWSd8EfePau7tUpfqWI1vibgdHjVWsKpRUki9zQ8LOPj8/FbU+nlSFMhZXHaQj1J54sTUNinuY8/2ECGsm8kCHtPYnM6A8lF092tx8Buo5L1aM1DxSPZKex3Q3N1YMWP0CJP1qWFXRYR9puWgas3uFZh7iEJDuFgvvmXl6AQ4jJ6S+jsxt5xrfdeYodxvzmZ9f09xjdnSRY6QM8NBnMgWBQRzvrim2zagfHrUmoSXK+ji7pGpl7JbT6Sgxa0p5a5LMmnPcNrawoL3WWSG7AVhaJC+WSTDXsy3Szy3SiUSsxVnNeE1LZfIkgUtob72xDGNm4nbpbiVrEfQcr1lS/cjGNAwf3o12ZZlycd53OclJWOBLeM5br+cXaW9Isn8QpVmDh9CErp02acajRiCBlvmkEEg6P21e9klzc7AB4Rgxa0eTzqYzCVIdIi1gTnzYj2nPmjUGKx18D50/1Az6mhnGuZvGOAkLwgP0PykWkhyzG/D1RVxX3NH7o8Zfuk/jSUPO63DoT2utJWRqb0IOWvKZBJkI0c811b6JNbSkgax+mCnRo68j4t/5ztFLv75IpQCxKU16uX8XI5dKXnBP2ukZdmvxxT9SlK/8NvThujTMBgfXsL3R8ndqT5PuV2OkjiETGh4tWsyb1IjqTl9xDCR2+47Olk+6x8sIox1vJ9jxTO+p5QUESnOb7RnOLBlOU555lxrrjN/x91m3wMxSftVvVD7KsP9ApdTwK9d9X3o3aph08VfrciPPw4qtmxTPfvOjZi0rR01dirpnLq+gYigZnHf0exFH763KM+PjWcZe/AXZB0drCKxV9DqWygaWHWG5+4zJBdkpmywWhJjHNcg5AYPq7XonUOGM0tggxRLNLulHhOnBRKIEX4p8prsHUC3N7Ybr61yAHVkcfwnPs+qYQGabbj3WXMCruYZXVqwD/VhkhLlySr72WSSxn5835jPQRTm0BogSXfNlqjsNYx2WpgfnU8thC3l+pLcKop5w2JG9UK+9L3DJYolRtNJz2ZfrVAzR/AsOFmlzoL2wHOFHKk9DgJm1hPYMYxyS6txXZUIBhvwQFpILA6pjFfGFSUCOmaGxhxqriT7ivEJ7xVC1uKTXBcuF/mrS+vhW8JTP4MlpRl4hpTXHj2C15NVFgEuWxksMLM7I92HyaShyXOkchTkaeY+Qy2SwnNkcp70CK73PIWIawbaoqvQp9E2Vgeczoa3v+r6XMz3QY/kVe7hYq6dYJMeKK4eJHum1wqUKC1961Of3dFU7+GPU9yt70sBYZxmLPk9GLf28sBfM5m8NeR8x3M4hYcfsfc2K5TW8kydFCKVD1L7mSuwWgsL9ygkv1Bpa1d0Zzq2VbqFzigKFTaZwQF4aHhro6Z8/Scfr0TZAFctElSwP47/YU8XRa77eNlms5JFaBvK4pNySoZgOO1od+Tx4NuIOM0wlmlFAEWk1TLSfSGGR/6jbNgA8YvDLhluZltw80gTL6XhK0XW12lgu82fb97QY0lt/sAxsyDeUTnH5ZpbcHEkHtBfYBb1GHDdM2nUSk2aCCdPWaqgLqb+kSPHHQj5U9NbEMqq0/m89AUVp21EQJnBpcvaHQPQb25vlSa/eyLwY33diHwn7wSP73F26uAa10spH8LzChND8bi4beNwAIAOMmMOJ6FbaHtnmJPwn4UlUgD2ApjfU0+Dvtvb/lgEiTwX4o7JiuNqhmjRbs+DgLgwV8Qtu5miL/XIPgWxw3HUWKQGoatSlimUhT3Qsfk2oU+WzmTeOMhAqQ2A/o86Xug6lqrugAI2tW1E4leTy6QmT/DGEvdUhwJQ2DEBs8Bom9X88EmRilLnLrQ0tZRkGknJ8Ibk1q6yR8FKHkAdK4RBGw3S+ebPbUJ1HtxX/2naE+o/kFCV9UTLeC7iE3eIn8tWnBRq+HsKW8QMJCjTKa9EmqjEDk8upyhtLABpCmgyKhbmrk6/X9QoPpP89o4Q0UdSFeL2tu53B0Q0yQkKGT4Yah8mOYIJlOjGEyGkIDxyBT5p6uhWK1KwFkp0APUTi6np+LzjAWXiqpZOf8+kxRm9dkdBy3ZxjDqTGJ+szKBdYEmeDryVRky1jmA8F8KHcA9StW2llrP3HHVvPGcR7Nl86csafyj+qIBWFdSU/V6pZXjHQokdiZTHhNU9IZWwMspVdbftu5HoCcvApVh62oerRwLC9+M5ffx6aj1nvZPO7M7RvbE74gxg5/oNaansVE3eb5AU4zKUBP2ez1N+rwEGJhD1FmRZkLQacwNjjizC0Bb5S9IayPtkloA2tZgbv6S0WpT9yd9cUh9T27G+cut5qLM7JaQD4wc1M0VpdY/AGKaKsFn2AKWUxvyokKaFuoYpXQvdFP5GAfO/WVcFV1K5mcbWXHIHD3HK5es0PNRjM1GfvVaNy2DNI/AYdU25iDKSm5XmOHsMyLwSPyAgeKqHmyi97AJR2f0bCsL5gHAJshly6/JQmT+pLDTT+1aWqWLRm/Rs80CUvMT+kNrheN6I4z7H7r+HwCYEdbVMWhTpoyM3F/NgT+2w9Gtn+FOy5wiGesKfCPVTwelOBe9wfeOkMjFm/5XCLwSZr1VbMYixalhyFHJseLgL6MWqL6b6cI1FbKjJld+SxkboxdIXsJiLYQGFWCqRyrFsQmvwTrrGUrZ5d5NmArAYaBFKRbCbuVrIzzhJ/tgOwWHfTTEn6sluqyQKA8xo7A8nbA4C6WqXokM9EZDYT2JMrSlUI9gYRBoPq2SXXOJL5EnhzWHmVbDci4uCulhMot1iCjdoInuLKJ1ZyUPvameZ15MQhkYMcUz5TIJmodue2wnk3lGpPjk+vfjfyZzcx5boBX/bLMnocZueRCE+pOTr4suxI7IYiArxQAlxzgSSDLADKHQmKG6PXogkeq9L182oKM+bK33MWT0JODqW2O7u3xglOi4yU+Q2iykWtUynpy/NOZFn4ulze3Mq9A3uroEbPo0vmvMVC1sDFJN3XNRR+aRNTbzuYI+ukIRK3QUN1d0TP1cl2nnB/C9pzWaxPtmxTM3Xyg9XsJrrHaXqQiO1wbanZyyqcEqTbbiZp0+V5vR3/zfsHa7/zt1uRpO8KuoMGBZ+FaLMCUype4yUyqqxSlLpzGI/f2/gM8zoB/wxkc2SJwnEYsp2lPA/JKWjrhUU9hXvVm7iD4p7TRc+Eyag+jpEu6XSXi+FNGqTZVokGzLiCvREFs3OSIjMZ09USPD+SVChQBhQnNkBOCelXYIX5tYsSUHekoYw2xqbMA+J9aH4cDk4RZzxjFqorIFkt26HU/HWQ2XTsqL5uZ6m3gR421O4Y5B+tfUwL65DNbY/ChycxjKytI3EhVUf3qvqGOrE9JZpiFrgEacGc/17j51iM/6XiZOihitryfq4VQLVGPK2ZsAbRMvoatBOGExhIMRALnkCylr+KADE5kDOObzO1r1ydIbt4rQ8p+Uivezw21DDNDDV2P5aG0HDmZ6VAnQR4776usnfe6xty6VyqXZUGSs5yOuPQa21tvZiq5LkBQIY9rhRk4nCFrNx//9hMOKw6VGKhdJMcyFtOu1lwEzU8xHXKvdGftGu9nl6PrRq5sH8jNhbkoiDfzlPSTxZFYn5yaResQPV9Upk37c3a5MNMQkuNXrM38WUS9WoR361kbzxBqMPR8EtEaL0sN6rDYrI1cEEK8rW7iacKTyE6n5gxkddykLE83D34/x5nks6cjuFDZ1umvXGRmaQPIjUHOcwoIXIIrSyfJ4R9bhqs4gfexpj27+qE2S3qmmHiDrTNlzrfTnlWdMXDtqUolNGRDk70ct+R+paxS7mrfSl8zHyVwDBthpFAYyiK50pzWPW4ff2LsPwMWvqbhXJIJXRmDW984I/P7je4Jq4y4/7ZZOmNKCPvR7iR33StASA/D3I1nCfmMC0jRyDoo9UB+hHYoC8ZHxAtyxkTOHIVSeF2X,iv:mShdle491Y1WB9R9xKfV+0vPincPw6C1X/XK25LKSfM=,tag:Wodrw/KYmLmJO0CWPOMcVA==,type:str]",
+ "sops": {
+ "age": [
+ {
+ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5R1Y2ekN1em0wUE9HSWdW\nWG9DUVJNRHV0d05TS0Q4SHdCR1hKeDlXR2lvCnBQVlAyOWNsOWh5M2dja0FFbExy\nMjVNeGlUZ0VvVDY0NHcwMEtTM2JUdVUKLS0tIDJkUk5EWURNc0NTRmlGbW84M0hn\nMFdzTVJsOVpIT0c3TXBxaFhRMm5XbFUKIOH1OT17DetrxMP/dmnuZ63O4BcWX/RG\n8lTDqkMmyvSkqpS56VdnYMAgRuHSu6hwbKubO/kufVSn1PKFKuYcog==\n-----END AGE ENCRYPTED FILE-----\n",
+ "recipient": "age1pu5wcvqqh92xr6ces8zmj4xvs52v7yfm4jsfcxm3z82a32u9n97qesldfv"
+ }
+ ],
+ "lastmodified": "2026-10-09T01:33:35Z",
+ "mac": "ENC[AES256_GCM,data:3UwI7Y7DJPOPC36arsxC8K3kAQ3RFucmr1Ah6FPwEKXellZ0KIFFDrljyRI6g+Ejp2vxm/VplJveWkkT2tBhTWp7g1hyWGaPc+9V2BJ4dcL0lJciqGcs3vO3v6L+GUEPdJTrHYkXLF/oc8Fp4i5v8b+AdtAcyP206E6J7desOqM=,iv:FOHVjpTyKij/s7VJ/TFprJD93DMEeEIj7Zm2WpS7dlA=,tag:9uGxZFoX/BgHUsbz5khh7g==,type:str]",
+ "version": "3.13.3"
+ }
+}