WRKLOG-2026-07-01

Summary

Wednesday. Fill summary at end of day.

URGENT - All Domains

Carryover Backlog (CRITICAL)

Task Details Origin Days Status

MSCHAPv2 Migration Report

Report due. 6-sheet Standard Report (exec summary, trend, waves, device detail, stale, policy match). Sheet 6 added 05-14: policy match by protocol for removal planning + anonymous identity validation. Migration window 2026-05-04 to 2026-05-30. ~6,227 devices, 5 waves.

2026-04-17

83

P0 - DUE — run report this week

Abnormal Security — ✅ COMPLETE

CR-2026-05-07-abnormal-read-write. CAB approved 2026-05-12. Implemented successfully 2026-05-13. Read/write enabled for pilot group. Post-deployment validation pending.

2026-05-07

63

✅ IMPLEMENTED — post-validation pending

SIEM QRadar → Sentinel Migration

Lead role. Monad console error RESOLVED 2026-05-12 — secrets configured in CHLA production tenant. ISE secure syslog integration in progress — cert imported, remote logging target configured, streaming errors under investigation. Blocking: DCR not created (Rule ID + Stream Name). Azure private network policy unresolved. Victor + Mauricio action.

2026-04-10

90

P0 - ACTIVE — ISE syslog + DCR blocking

Monad Pipeline Evaluation

Sentinel output connector. Console error resolved. 3 of 6 values configured. Remaining: Endpoint URL (have it), Rule ID + Stream Name (need DCR). ISE Remote Logging Target configured 2026-05-18 — TLS cert imported, secure syslog target created. Streaming errors in Monad console under investigation.

2026-03-11

120

P0 - ACTIVE — ISE integration in progress

Guest Redirect ACL

Guest redirect ACL work needed. Related to Mandiant remediation findings.

2026-05-12

58

P0 - TODO

ISE Patch 10 (CVE-2026-20147 CVSS 9.9)

ISE 3.2 Patch 10. Supersedes Patch 9. 61 days on a CVSS 9.9 — schedule maintenance window. Write CR if needed.

2026-03-12

119

P0 - OVERDUE — schedule immediately

k3s NAT verification

NAT rule 170 for 10.42.0.0/16 pod network - test internet connectivity. 64 days — test this week or defer to Q3.

2026-03-09

122

P0 - BLOCKING — TRIAGE: schedule or defer

Wazuh indexer recovery

Restart pod after NAT confirmed working - SIEM visibility blocked. Blocked by k3s NAT — cannot proceed until above resolved.

2026-03-09

122

P0 - Blocked by k3s

Strongline Gateway VLAN fix

8 devices in wrong identity group (David Rukiza assigned)

2026-03-16

115

P0 - TODO

TCP Clocks deployment

ISE identity group validation, query outputs, comms with team. Active d001 data Apr 22-23.

2026-04-22

78

P0 - ACTIVE

IoT Dr. Kim — recurring

Sleep study devices (Apr 15-16), watches recurrence (Apr 22). 5 incident versions in d001. Validate iPSK enrollment.

2026-04-15

85

P0 - RECURRING

Murus Portae (WAF) — Phase 0

FMC cert expired, ACP returns zero rules. d001: zone map, architecture D2, FMC API reference, ops script.

2026-04-16

84

P0 - INVESTIGATING

Vocera EAP-TLS Supplicant Fix

~10 phones failing 802.1X, missing supplicant config. 61 days — schedule with clinical engineering team.

2026-03-12

119

P1 - TODO — schedule

ISE MnT Messaging Service

Enable "Use ISE Messaging Service for UDP syslogs delivery". 61 days — low risk, schedule with ISE Patch 10 maintenance window.

2026-03-12

119

P2 - BUNDLE with Patch 10

Professional backlog remains critical. Check Days column for priorities.

BLOCKERS — Fix Immediately

Task Details Origin Days Impact

Z Fold 7 Termux

gopass and SSH not working

2026-03-10

58

BLOCKER — Cannot access passwords on mobile

gopass v3 organization

Inconsistent structure, poor key-value usage

2026-03-20

48

Inefficient password management, no aggregation

Git history scrub — sensitive personal terms

Plaintext references to personal legal matters in committed worklogs (WRKLOG-2026-03-14, WRKLOG-2026-04-18). Forward-fixed but old commits still contain strings. Requires git filter-repo + force-push. See runbook below.

2026-04-22

15

SECURITY — sensitive terms in public git history

Runbook: Git History Scrub (d000 Personal Terms)

Problem: Two committed worklogs contained plaintext references to personal legal matters. The files have been edited (forward-fix), but git history retains the original text in prior commits.

Affected commits: Any commit touching these files:

# Identify affected commits
git log --oneline -- \
  docs/modules/ROOT/pages/2026/03/WRKLOG-2026-03-14.adoc \
  docs/modules/ROOT/pages/2026/04/WRKLOG-2026-04-18.adoc

Scrub procedure:

# 1. BEFORE: Full backup of the repo
cp -a ~/atelier/_bibliotheca/domus-captures ~/atelier/_bibliotheca/domus-captures.bak

# 2. Install git-filter-repo (if not present)
# Arch: pacman -S git-filter-repo
# pip: pip install git-filter-repo

# 3. Create expressions file for replacement
cat > /tmp/scrub-expressions.txt << 'EXPR'
regex:(?i)divorce==[REDACTED]
regex:(?i)dissolutio(?!n\.adoc\.age)==[REDACTED-LEGAL]
regex:(?i)iliana==[REDACTED-NAME]
regex:(?i)angulo-arreola==[REDACTED-NAME]
regex:legal-divorce-notes\.age==legal-notes.age
regex:1099-NEC-iliana==1099-NEC
EXPR

# 4. Verify before (dry run — count matches in history)
git log -p --all -S 'divorce' -- '*.adoc' | grep -c 'divorce' || echo "0 matches"
git log -p --all -S 'iliana' -- '*.adoc' | grep -c 'iliana' || echo "0 matches"

# 5. Run filter-repo (DESTRUCTIVE — rewrites all commit hashes)
git filter-repo --replace-text /tmp/scrub-expressions.txt --force

# 6. Verify after
git log -p --all -S 'divorce' -- '*.adoc' | grep -c 'divorce' || echo "0 matches — CLEAN"
git log -p --all -S 'iliana' -- '*.adoc' | grep -c 'iliana' || echo "0 matches — CLEAN"

# 7. Re-add remotes (filter-repo removes them)
git remote add origin git@github.com:<user>/domus-captures.git
# Add any other remotes (Gitea, etc.)

# 8. Force-push to all remotes (DESTRUCTIVE — overwrites remote history)
git remote | xargs -I{} git push {} main --force

# 9. Clean up
rm /tmp/scrub-expressions.txt
rm -rf ~/atelier/_bibliotheca/domus-captures.bak  # only after verifying

Post-scrub checklist:

  • Backup created before running

  • git filter-repo installed

  • Expressions file reviewed — no false positives (e.g., Don Quijote "Angulo el Malo" is in segunda-parte/texto/texto-011.adoc — the regex targets angulo-arreola specifically to avoid this)

  • Dry-run counts match expectations

  • Filter-repo executed

  • Post-scrub verification shows 0 matches

  • Remotes re-added

  • Force-pushed to all remotes

  • Cloudflare Pages rebuild verified

  • Local clones on other machines re-cloned or git fetch --all && git reset --hard origin/main

  • Backup removed

URGENT - Requires Immediate Action

Item Details Deadline Status Impact

Housing Search

Granada Hills area - apartments/rooms

TBD

In Progress

Quality of life, commute

2025 Tax — IRS Transcript Review

MFJ filed 2026-04-22. Pull IRS Return Transcript to verify contents. Consult attorney re: Form 8857 (Innocent Spouse Relief). Details in encrypted case file.

Before attorney meeting

In Progress

Financial — liability exposure. See encrypted D000 case file.

Rack Relocation

Physical move of server rack. CR written: CR-2026-04-18 (pending in infra-ops). Borg backup completed. VM XML dumps, switch save, shutdown/startup procedure documented.

TBD

Pending

Infrastructure downtime — all services offline during move

D000 Legal Planning

Encrypted D000 case file. Open: d000 open dissolutio. Close: d000 close dissolutio. 19 partials + assembler. PDF build for attorney handoff. Critical deadline: Jan 2029.

Before Jan 2029

Active — escalating

Life transition — see case file for details

Credit Report Review

Pull reports from all 3 bureaus via annualcreditreport.com. Verify no unknown joint accounts or debts. Credentials in gopass: v3/personal/finance/credit/annual_credit_report

TBD

In Progress

Financial discovery — FL-142 preparation

Gopass Security Audit

Rotate passwords on shared/known accounts. Add 2FA backup codes to v3/personal/recovery/. Create missing government entries (IRS, SSA, VA, DMV). Add last_login field to active entries.

TBD

Pending

Digital security — pre-filing preparation

Subscription Audit

Download 3 months bank/CC statements (Chase, NFCU, USAA). Identify all recurring charges. Cancel unnecessary. Document active subscriptions for FL-150.

TBD

Pending

Financial — expense documentation

401(k) Enrollment

Enroll in CHLA 401(k) immediately. Post-separation contributions are 100% separate property. Reduces gross income for support calculations. Max 2026: $23,500/yr.

In progress (started 5/4)

In Progress

Financial — support calculation + retirement

URGENT — Performance Review Certifications

Certification Provider Deadline Status Impact

CISSP

ISC² — Certified Information Systems Security Professional

July 12, 2026

ACTIVE — Week 2 of 10 (Project)

Required for performance review. 10-week accelerated plan.

RHCSA 9

Red Hat Certified System Administrator

Q3 2026

ACTIVE — 21-phase curriculum (Project)

After CISSP. Required for performance review.

CISSP: 41 days remaining (exam July 12). Domain 1 study in progress. Schedule exam today (06-01).

Early Morning - 5:30am

Regex Training (CRITICAL CARRYOVER)

  • Session 3 - Character classes, word boundaries

  • Practice drills from regex-mastery curriculum

  • Status: 52 days carried over (since 2026-03-16) — CRITICAL

Regex training continues to slip. This is the foundation for all CLI mastery.

Daily Notes

Triage Status

Item Status Destination

first item

pending

destination

palaestra-cli-challenge

Palaestra Digitalis — CLI Challenge

Palaestra Digitalis — 2026-07-01

Challenge and drills go here. Curriculum arc: grep → awk → C → CTF.

Z Fold 7 — Full Workflow Deploy

Z Fold 7 — Full Workflow Deploy

New device connected via ADB (R3GYB0J7YHY, SM_F966U1). Old Termux state is gone — deploy the full Linux workflow from scratch.

Scope: Termux baseline, SSH, repos (domus-captures, dots-quantum, domus-asciidoc-build, others), gopass v3+v4, stow, build-adoc, mobile aliases.

Prior art:

  • Z Fold 7 Mobile Workflow — phases 0-10 (reference, not repeat)

  • INC-2026-03-16 — gopass/SSH cert failure (lessons: F-Droid only, u0_a385 principals)

Phone File Organization via ADB

Run before Termux setup. Pull all user files to the workstation, organize by category, clean the device.

Verify ADB connection
adb devices -l
Inventory the phone
adb shell ls -la /sdcard/
adb shell ls -la /sdcard/Download/
adb shell ls -la /sdcard/DCIM/Camera/ | wc -l
adb shell ls -laR /sdcard/DCIM/Screenshots/
adb shell ls -laR /sdcard/Pictures/
adb shell ls -laR /sdcard/Documents/
adb shell ls -laR /sdcard/Movies/
adb shell ls -laR /sdcard/Recordings/
adb shell ls -laR /sdcard/Music/
adb shell ls runs on the device — no run-as needed for /sdcard/. Termux’s data dir (/data/data/com.termux/) is not accessible without root.
Create archive structure on workstation
mkdir -p ~/atelier/_captures/zfold7-pull-$(date +%Y-%m-%d)/{photos/camera,photos/screenshots,photos/messages,video,audio/recordings,literature,career,personal,infra,trash}
Pull files by category
PULL=~/atelier/_captures/zfold7-pull-$(date +%Y-%m-%d)

adb pull /sdcard/DCIM/Camera/ "$PULL/photos/camera/"
adb pull /sdcard/DCIM/Screenshots/ "$PULL/photos/screenshots/"
adb pull /sdcard/Recordings/ "$PULL/audio/recordings/"
adb pull /sdcard/Pictures/ "$PULL/photos/messages/"
adb pull /sdcard/Movies/ "$PULL/video/"

For individual files from Download/, pull by name:

adb pull /sdcard/Download/filename.pdf "$PULL/literature/"
Verify pull
find "$PULL" -type f | wc -l
du -sh "$PULL"/*/
Clean the phone
Quote the glob — zsh expands * locally before adb shell sees it.
adb shell 'rm -rf /sdcard/DCIM/Camera/*'
adb shell 'rm -rf /sdcard/DCIM/Screenshots/*'
adb shell 'rm -rf /sdcard/Recordings/*'
adb shell 'rm -rf /sdcard/Pictures/Mensajes/*'
adb shell 'rm -rf /sdcard/Pictures/Messages/*'
adb shell 'rm -rf /sdcard/Movies/*'
adb shell 'rm -rf /sdcard/Download/*'
Verify clean
adb shell 'find /sdcard/ -maxdepth 3 -type f 2>/dev/null | grep -v "/Android/" | grep -v "/.thumbnails/"'
2026-07-01 pull results
Category Files Size

photos/camera

138

7.5 GB

audio/recordings

10

155 MB

photos/messages

3

83 MB

literature

12

26 MB

video/tesla

1

15 MB

career

4

2.8 MB

infra

4

512 KB

personal

2

232 KB

Total

185

~7.7 GB

Removed from phone: nested quijote-mobile/quijote-mobile/ duplicate, orwell1984.pdf (dupe of 1984.pdf), Documents/Testobs/ (dead Obsidian experiment), Music/Samsung/Over_the_Horizon.m4a (stock ringtone).

Lessons learned
  • adb shell run-as com.termux fails — Termux is not a debuggable package

  • adb push to /data/data/com.termux/ fails — requires root

  • The bridge is /sdcard/ (shared storage) — ADB can read/write freely

  • Always single-quote globs in adb shell commands — zsh expands them locally otherwise

Package Audit

Run these on the phone (Termux) or via ssh fold7 before installing anything.

Full package count
pkg list-installed 2>/dev/null | wc -l
Upgradable packages
pkg list-installed 2>/dev/null | grep upgradable
Per-tool audit loop
TOOLS=(
  # === Shell ===
  zsh bash fish
  # === Editor ===
  neovim nano vim
  # === Multiplexer ===
  tmux
  # === Git ===
  git
  # === Search & Filter ===
  fzf fd ripgrep jq yq bat grep gawk sed
  # === Network ===
  curl wget rsync openssh nmap netcat-openbsd dnsutils iproute2 lsof w3m
  # === Crypto & Secrets ===
  gnupg pass gopass age openssl-tool
  # === Dev Languages ===
  python nodejs-lts rust golang clang make
  # === Dev Tools ===
  lua-language-server rust-analyzer luarocks strace
  # === LSP & Treesitter ===
  tree-sitter tree-sitter-parsers
  # === Prompt ===
  oh-my-posh
  # === File Management ===
  stow tree unzip tar
  # === Misc ===
  tealdeer tesseract aerc dos2unix
  # === Android Integration ===
  termux-api
)

printf "\n%-25s %-10s %s\n" "PACKAGE" "STATUS" "VERSION"
printf "%-25s %-10s %s\n" "-------" "------" "-------"

for tool in "${TOOLS[@]}"; do
  [[ "$tool" =~ ^#.*$ ]] && { printf "\n%s\n" "$tool"; continue; }
  ver=$(pkg show "$tool" 2>/dev/null | awk -F': ' '/^Version/{print $2; exit}')
  if dpkg -s "$tool" &>/dev/null; then
    upg=$(apt list --upgradable 2>/dev/null | grep "^${tool}/" | awk -F' ' '{print $2}')
    if [ -n "$upg" ]; then
      printf "%-25s %-10s %s → %s\n" "$tool" "UPGRADE" "$(dpkg -s "$tool" | awk -F': ' '/^Version/{print $2}')" "$upg"
    else
      printf "%-25s %-10s %s\n" "$tool" "OK" "$(dpkg -s "$tool" | awk -F': ' '/^Version/{print $2}')"
    fi
  else
    if [ -n "$ver" ]; then
      printf "%-25s %-10s %s\n" "$tool" "MISSING" "(available: $ver)"
    else
      printf "%-25s %-10s\n" "$tool" "NOT FOUND"
    fi
  fi
done
Binary check (what’s actually in PATH)
BINS=(
  zsh bash fish
  nvim nano vim
  tmux
  git
  fzf fd rg jq yq bat grep awk sed
  curl wget rsync ssh nmap nc dig ip lsof w3m
  gpg pass gopass age openssl
  python3 node rustc go clang make
  lua-language-server rust-analyzer luarocks strace
  oh-my-posh
  stow tree unzip tar
  tldr tesseract aerc dos2unix
  termux-api-battery-status
)

printf "\n%-25s %-10s %s\n" "BINARY" "STATUS" "PATH"
printf "%-25s %-10s %s\n" "------" "------" "----"

for bin in "${BINS[@]}"; do
  loc=$(which "$bin" 2>/dev/null)
  if [ -n "$loc" ]; then
    printf "%-25s %-10s %s\n" "$bin" "OK" "$loc"
  else
    printf "%-25s %-10s\n" "$bin" "MISSING"
  fi
done
SSH key inventory
printf "\n%-40s %-10s %s\n" "KEY" "PERMS" "TYPE"
printf "%-40s %-10s %s\n" "---" "-----" "----"

for key in ~/.ssh/id_*; do
  [ -f "$key" ] || continue
  perms=$(stat -c '%a' "$key")
  type=$(head -1 "$key" | awk '{print $1}' | sed 's/.*BEGIN //' | sed 's/ KEY.*//')
  printf "%-40s %-10s %s\n" "$(basename "$key")" "$perms" "$type"
done
Vault SSH cert status
if [ -f ~/.ssh/id_ed25519_vault-cert.pub ]; then
  echo "=== Vault SSH Cert ==="
  ssh-keygen -L -f ~/.ssh/id_ed25519_vault-cert.pub 2>&1 | grep -E 'Type|Valid|Principals'

  # Check if expired
  expiry=$(ssh-keygen -L -f ~/.ssh/id_ed25519_vault-cert.pub 2>&1 | awk '/Valid/{print $5}')
  now=$(date -u +%Y-%m-%dT%H:%M:%S)
  if [[ "$now" > "$expiry" ]]; then
    echo "STATUS: EXPIRED (expired $expiry)"
  else
    echo "STATUS: VALID (until $expiry)"
  fi
else
  echo "=== Vault SSH Cert ==="
  echo "STATUS: NOT FOUND"
fi
Repo status
REPOS=(
  ~/atelier/_bibliotheca/domus-captures
  ~/atelier/_bibliotheca/domus-infra-ops
  ~/atelier/_bibliotheca/domus-netapi-docs
  ~/atelier/_bibliotheca/domus-secrets-ops
  ~/atelier/_bibliotheca/domus-asciidoc-build
  ~/atelier/_projects/personal/dotfiles-optimus
  ~/atelier/_projects/personal/dots-quantum
  ~/.config/nvim
  ~/.secrets
)

printf "\n%-50s %-10s %s\n" "REPO" "STATUS" "LAST COMMIT"
printf "%-50s %-10s %s\n" "----" "------" "-----------"

for repo in "${REPOS[@]}"; do
  if [ -d "$repo/.git" ]; then
    last=$(git -C "$repo" log --oneline -1 2>/dev/null)
    behind=$(git -C "$repo" rev-list HEAD..origin/main --count 2>/dev/null)
    if [ "$behind" -gt 0 ] 2>/dev/null; then
      printf "%-50s %-10s %s (%d behind)\n" "$repo" "STALE" "$last" "$behind"
    else
      printf "%-50s %-10s %s\n" "$repo" "OK" "$last"
    fi
  else
    printf "%-50s %-10s\n" "$repo" "MISSING"
  fi
done
gopass mounts
echo "=== gopass mounts ==="
gopass mounts
echo ""
echo "=== v3 entry count ==="
gopass ls --flat v3 2>/dev/null | wc -l
echo ""
echo "=== v4 entry count ==="
gopass ls --flat v4 2>/dev/null | wc -l
echo "=== Stow symlinks ==="
for f in ~/.zshrc ~/.zshenv ~/.zprofile ~/.gitconfig ~/.fzf ~/.vimrc; do
  if [ -L "$f" ]; then
    target=$(readlink "$f")
    if [ -e "$f" ]; then
      printf "%-20s → %s\n" "$(basename "$f")" "$target"
    else
      printf "%-20s → %s (BROKEN)\n" "$(basename "$f")" "$target"
    fi
  elif [ -f "$f" ]; then
    printf "%-20s (not symlink — stow conflict)\n" "$(basename "$f")"
  else
    printf "%-20s MISSING\n" "$(basename "$f")"
  fi
done
SSH config state
echo "=== SSH config ==="
head -3 ~/.ssh/config
echo ""
echo "=== config.d/ ==="
ls -la ~/.ssh/config.d/ 2>/dev/null || echo "NOT DEPLOYED"
Phone Directory Cleanup

Run before cloning repos or stowing dotfiles. Removes drift from the March 2026 setup.

Enumerate current state (on phone)
echo "=== Home directory ==="
ls -la ~/

echo ""
echo "=== ~/.config/ ==="
ls ~/.config/

echo ""
echo "=== ~/.local/share/ ==="
ls ~/.local/share/

echo ""
echo "=== ~/.local/state/ ==="
ls ~/.local/state/

echo ""
echo "=== atelier tree ==="
find ~/atelier -maxdepth 3 -type d | grep -v '.git' | sort

echo ""
echo "=== Stale dirs outside atelier ==="
for d in ~/downloads ~/src; do
  [ -d "$d" ] && echo "STALE: $d" || echo "clean: $d"
done

echo ""
echo "=== Stale files in ~ ==="
for f in ~/cprom.adoc ~/test.lua; do
  [ -f "$f" ] && echo "STALE: $f" || echo "clean: $f"
done
Remove duplicate nvim clones (on phone)

~/.config/domus-nvim was a duplicate clone of ~/.config/nvim. Remove the duplicate, its data dirs, and the broken symlink inside nvim.

dots-quantum sets NVIM_APPNAME=domus-nvim in zshrc, so nvim looks for ~/.config/domus-nvim/. After removing the duplicate, create a symlink back to ~/.config/nvim/ — without it, nvim falls back to netrw (no config found).
rm -rf ~/.config/domus-nvim
rm -rf ~/.local/share/domus-nvim
rm -rf ~/.local/state/domus-nvim
rm -rf ~/.local/share/nvim-domus
rm -rf ~/.local/state/nvim-domus
rm -f ~/.config/nvim/nvim-domus
ln -s ~/.config/nvim ~/.config/domus-nvim
Remove leftover starship config (on phone)

oh-my-posh is the prompt, not starship:

rm -f ~/.config/starship.toml
Remove stale directories (on phone)
rm -rf ~/downloads
rm -rf ~/src
Remove stale files (on phone)
rm -f ~/cprom.adoc
rm -f ~/test.lua
Verify clean (on phone)
echo "=== ~ ==="
ls ~/

echo ""
echo "=== ~/.config/ ==="
ls ~/.config/

echo ""
echo "=== ~/.local/share/ ==="
ls ~/.local/share/

echo ""
echo "=== ~/.local/state/ ==="
ls ~/.local/state/

Expected after cleanup:

  • ~/.config/ — aerc, domus-nvim (symlink → nvim), fish, git, gopass, nvim, omp-theme.toml, shell

  • ~/.local/share/ — gopass, nvim, zsh

  • ~/.local/state/ — nvim

  • ~/ — .config, .gnupg, .local, .ssh, .termux, atelier, storage

Termux Baseline — Audit & Fix-Up

Phone had Termux from March 2026 setup. Partially configured, drifted. This documents the actual state found and the fix-up commands.

Audit results (2026-07-01)
Item State Action

Termux (F-Droid)

✅ Installed

Termux:API

✅ Installed

Termux:Boot

❌ Not verified

Install from F-Droid

sshd

✅ Works

Must start manually after reboot

oh-my-posh

✅ Installed

GPG key

✅ Valid (expires 2027-11-30)

gopass v3

✅ Syncs

Passphrase prompt for GitHub key

gopass v4

❌ Not mounted

gopass mounts add v4

Dotfiles

⚠️ Stowed but fzf/fd/rg symlinks broken

Re-stow after installing packages

SSH keys

✅ All named keys present

SSH config

⚠️ Flat 28KB file, no config.d

Replace with modular config.d

Vault SSH cert

❌ Expired 2026-03-18

Re-sign from Vault

Repos

⚠️ 3 of 9 present, all stale

Pull + clone missing

Install missing packages (idempotent)

Check first, install only what’s missing:

for pkg in neovim tmux fzf fd ripgrep yq wget golang nmap netcat-openbsd git-lfs; do
  command -v "${pkg}" &>/dev/null || {
    echo "--- Installing $pkg ---"
    pkg install -y "$pkg"
  }
done
Binary names differ from package names for some tools. Verify with the binary check:
for bin in nvim tmux fzf fd rg yq wget go nmap nc; do
  printf "%-10s " "$bin"
  command -v "$bin" 2>/dev/null || echo "MISSING"
done
Upgrade stale packages
pkg upgrade -y
ADB bootstrap model (for fresh installs)

On a brand new phone with no Termux, the bootstrap splits across two surfaces:

Surface What runs here Why

Phone (Termux)

pkg install, passwd, sshd, termux-setup-storage, whoami

Only Termux’s own shell can write to its data dir

Workstation (ADB)

adb push to /sdcard/, adb devices, adb shell 'ip addr show wlan0'

Shared storage is the bridge

Phone (Termux)

cp ~/storage/shared/file ~/

Move from shared storage into Termux home

adb shell run-as com.termux fails — Termux is not a debuggable package. adb push to /data/data/com.termux/ fails — requires root. The only ADB-writable path is /sdcard/ (shared storage).
Fresh install packages (reference — full list)
pkg install openssh git neovim tmux curl wget rsync stow \
  pass gnupg age \
  python nodejs golang rust \
  jq yq fzf fd ripgrep bat \
  nmap netcat-openbsd dnsutils \
  termux-api oh-my-posh
Auto-start sshd (if Termux:Boot installed)
mkdir -p ~/.termux/boot
cat > ~/.termux/boot/start-sshd.sh << 'EOF'
#!/data/data/com.termux/files/usr/bin/bash
sshd
EOF
chmod +x ~/.termux/boot/start-sshd.sh
Get phone IP from workstation via ADB
adb shell 'ip addr show wlan0' | awk '/inet / {split($2,a,"/"); print a[1]}'
Authorized keys (fresh install only)

The phone needs your public key to accept incoming SSH from the workstation. Push via ADB to shared storage, then copy into Termux:

From workstation:

adb push ~/.ssh/id_ed25519_d000.pub /sdcard/authorized_key.pub

On phone (Termux):

mkdir -p ~/.ssh
cat ~/storage/shared/authorized_key.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
rm ~/storage/shared/authorized_key.pub
SSH — config.d Migration & Vault Cert
Current state (2026-07-01)
Item State Action

SSH keys (all named)

✅ Present

authorized_keys

✅ Present

SSH config

⚠️ Flat 28KB file from March

Replace with config.d

config.d/

❌ Does not exist

Deploy from workstation

Vault SSH cert

❌ Expired 2026-03-18 (8hr TTL)

Re-sign

Git SSH (GitHub)

✅ Works (gopass sync confirmed)

Step 1 — Replace flat config with config.d

From workstation — push config.d:

scp -r ~/.ssh/config.d fold7:~/.ssh/

On phone (Termux) — back up the flat config first, then replace with Include:

wc -l ~/.ssh/config
cp ~/.ssh/config ~/.ssh/config.bak.2026-07-01
echo 'Include config.d/*.conf' | tee ~/.ssh/config > /dev/null
chmod 600 ~/.ssh/config
zsh > redirection fails with "file exists" on existing files (clobber protection). Use tee instead of heredoc/redirect.

Verify config.d was copied:

ls -la ~/.ssh/config.d/

Verify the modular config loads:

ssh -G fold7 | grep -E 'hostname|user|identityfile|port'
Step 2 — Re-sign Vault SSH cert

The cert expired 2026-03-18 (8hr TTL). Termux user is u0_a385.

On workstation:

ds d000 dev/vault

Check cluster health:

for node in vault-01 vault-02 vault-03; do
  printf "%-12s " "$node"
  curl -sk "https://$node.inside.domusdigitalis.dev:8200/v1/sys/health" \
    | jq -r '"\(if .sealed then "SEALED" else "unsealed" end)  \(if .standby then "standby" else "ACTIVE" end)  v\(.version)"' \
    2>/dev/null || echo "UNREACHABLE"
done

Sign with both principals:

vault write -field=signed_key ssh/sign/domus-client \
  public_key=@$HOME/.ssh/id_ed25519_d000.pub \
  valid_principals="u0_a385,evanusmodestus" \
  cert_type=user | tee /tmp/zfold7-ssh-cert.pub > /dev/null

Verify:

ssh-keygen -L -f /tmp/zfold7-ssh-cert.pub

Confirm u0_a385 AND evanusmodestus appear in Principals.

Push via ADB:

adb push /tmp/zfold7-ssh-cert.pub /sdcard/

On phone (Termux):

cp ~/storage/shared/zfold7-ssh-cert.pub ~/.ssh/id_ed25519_d000-cert.pub
chmod 600 ~/.ssh/id_ed25519_d000-cert.pub
rm ~/storage/shared/zfold7-ssh-cert.pub

Test against a Vault-trusting host:

ssh kvm-02

Cleanup on workstation:

rm -f /tmp/zfold7-ssh-cert.pub
Include BOTH the Termux user AND evanusmodestus in valid_principals. INC-2026-03-16 root cause: hardcoded old Termux user (u0_a361 vs u0_a385).
Step 3 — Verify all connectivity from phone
ssh -T git@github.com
ssh -T git@gitlab.com
ssh -T git@gitea-01
ssh kvm-02 hostname
Workstation SSH config — fold7 entry with Vault cert chain

The fold7 entry needs the full key hierarchy so Vault cert auth is tried first.

Verify current state:

awk '/^Host fold7/,/^$/' ~/.ssh/config.d/80-d000-hosts.conf

Update — delete the old block, append the new one. sed range /,/^$/ is unsafe on the last block (no trailing blank line = wipe).

# Verify before
awk '/^Host fold7/,/^$/' ~/.ssh/config.d/80-d000-hosts.conf

# Delete the old fold7 block (safe: grep first to confirm it exists)
grep -q '^Host fold7' ~/.ssh/config.d/80-d000-hosts.conf && \
  sed -i '/^Host fold7$/,/^$/d' ~/.ssh/config.d/80-d000-hosts.conf

# Append the new block
cat >> ~/.ssh/config.d/80-d000-hosts.conf << 'EOF'

Host fold7
    HostName 10.50.10.145
    Port 8022
    IdentityFile ~/.ssh/id_ed25519_vault
    CertificateFile ~/.ssh/id_ed25519_vault-cert.pub
    IdentityFile ~/.ssh/id_ed25519_d000
EOF

# Verify after
awk '/^Host fold7/,/^$/' ~/.ssh/config.d/80-d000-hosts.conf

Test config resolution:

ssh -G fold7 | grep -E 'hostname|user|identityfile|certificatefile|port'

Delete stale .age, re-encrypt:

rm ~/.ssh/config.d/80-d000-hosts.conf.age
age -e -R ~/.age/recipients/self.txt -o ~/.ssh/config.d/80-d000-hosts.conf.age ~/.ssh/config.d/80-d000-hosts.conf

Verify with forensic stat — .age must be newer and larger than plaintext:

stat --printf='%n\n  Size: %s bytes\n  Modified: %y\n  Permissions: %A (%a)\n  Inode: %i\n' ~/.ssh/config.d/80-d000-hosts.conf ~/.ssh/config.d/80-d000-hosts.conf.age

Confirm file type and round-trip integrity:

# Confirm age encryption (magic bytes)
file ~/.ssh/config.d/80-d000-hosts.conf.age

# Decrypt → diff against plaintext (process sub — never writes to disk)
diff <(age -d -i ~/.age/identities/self.txt ~/.ssh/config.d/80-d000-hosts.conf.age) \
     ~/.ssh/config.d/80-d000-hosts.conf \
  && echo "MATCH" || echo "DRIFT"

Kill stale sessions and mux sockets, then test:

kill $(lsof -i @10.50.10.145 -t) 2>/dev/null
rm -f ~/.ssh/sockets/*10.50.10.145* 2>/dev/null
ssh fold7
Never test SSH with ssh -v fold7 2>&1 | grep — the pipe leaves sessions stuck in ESTABLISHED state. Test with a plain ssh fold7 and observe the passphrase prompt (vault key = working, d000 key = fallback).
Troubleshooting
  • Connection refused — sshd not running. Open Termux on phone, run sshd

  • Permission denied with no key promptIdentityFile missing in SSH config

  • Permission denied after passphraseauthorized_keys missing or wrong permissions on phone

  • Stale mux socketkill $(lsof -i @10.50.10.145 -t) 2>/dev/null && rm -f ~/.ssh/sockets/10.50.10.145 2>/dev/null

  • Stuck sessions from piped ssh — never ssh -v host 2>&1 | grep. Use plain ssh host to test.

  • Vault cert rejected — Termux user not in valid_principals

From-scratch reference (no prior SSH state)

For a phone with no SSH keys at all, push the authorized key and private keys via ADB:

From workstation:

adb push ~/.ssh/id_ed25519_d000.pub /sdcard/authorized_key.pub

On phone (Termux):

mkdir -p ~/.ssh
cat ~/storage/shared/authorized_key.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
rm ~/storage/shared/authorized_key.pub

Then add the fold7 entry with IdentityFile to the workstation SSH config and test.

Repo Inventory — Audit & Sync
Enumerate directory structure (on phone)
echo "=== Phone atelier tree ==="
find ~/atelier -maxdepth 3 -type d 2>/dev/null | grep -v '.git'
Compare against workstation (on workstation)
echo "=== Workstation atelier tree ==="
find ~/atelier -maxdepth 2 -type d | grep -v '.git' | sort
Create missing directories (on phone)
mkdir -p ~/atelier/_bibliotheca
mkdir -p ~/atelier/_projects/personal
mkdir -p ~/.secrets
Enumerate repos (on phone)
REPOS=(
  ~/atelier/_bibliotheca/domus-captures
  ~/atelier/_bibliotheca/domus-infra-ops
  ~/atelier/_bibliotheca/domus-netapi-docs
  ~/atelier/_bibliotheca/domus-secrets-ops
  ~/atelier/_bibliotheca/domus-asciidoc-build
  ~/atelier/_projects/personal/dots-quantum
  ~/.config/nvim
  ~/.secrets
)

printf "\n%-55s %-10s %s\n" "REPO" "STATUS" "LAST COMMIT"
printf "%-55s %-10s %s\n" "----" "------" "-----------"

for repo in "${REPOS[@]}"; do
  if [ -d "$repo/.git" ]; then
    last=$(git -C "$repo" log --oneline -1 2>/dev/null)
    printf "%-55s %-10s %s\n" "$repo" "OK" "$last"
  else
    printf "%-55s %-10s\n" "$repo" "MISSING"
  fi
done
Current state (2026-07-01)
Repo State Action

dots-quantum

❌ Not cloned

git clone + stow (see dots-quantum-inventory)

dotfiles-optimus

⚠️ Present (deprecated)

Unstow, then remove after dots-quantum is stowed

domus-infra-ops

⚠️ Stale (Mar)

git pull

domus-nvim (~/.config/nvim)

⚠️ Stale (Mar)

git pull

domus-captures

❌ Not cloned

git clone

domus-secrets-ops

❌ Not cloned

git clone

domus-asciidoc-build

❌ Not cloned

git clone

domus-netapi-docs

❌ Not cloned

git clone

~/.secrets

❌ Not cloned

git clone

Pull stale repos (on phone)
git -C ~/atelier/_projects/personal/dots-quantum pull
git -C ~/atelier/_bibliotheca/domus-infra-ops pull
git -C ~/.config/nvim pull
Clone missing repos (on phone)
command -v git-lfs &>/dev/null || pkg install -y git-lfs
git lfs install
git clone git@github.com:EvanusModestus/domus-captures.git ~/atelier/_bibliotheca/domus-captures
git clone git@github.com:EvanusModestus/domus-secrets-ops.git ~/atelier/_bibliotheca/domus-secrets-ops
git clone git@github.com:evanusmodestus/domus-asciidoc-build.git ~/atelier/_bibliotheca/domus-asciidoc-build
git clone git@github.com:EvanusModestus/domus-netapi-docs.git ~/atelier/_bibliotheca/domus-netapi-docs
mkdir -p ~/.secrets
git clone git@github.com:EvanusModestus/secrets.git ~/.secrets
Verify all repos
REPOS=(
  ~/atelier/_bibliotheca/domus-captures
  ~/atelier/_bibliotheca/domus-infra-ops
  ~/atelier/_bibliotheca/domus-netapi-docs
  ~/atelier/_bibliotheca/domus-secrets-ops
  ~/atelier/_bibliotheca/domus-asciidoc-build
  ~/atelier/_projects/personal/dots-quantum
  ~/.config/nvim
  ~/.secrets
)

for repo in "${REPOS[@]}"; do
  if [ -d "$repo/.git" ]; then
    printf "%-60s %s\n" "$repo" "$(git -C "$repo" log --oneline -1 2>/dev/null)"
  else
    printf "%-60s MISSING\n" "$repo"
  fi
done
From-scratch reference (no repos at all)
mkdir -p ~/atelier/_bibliotheca
mkdir -p ~/atelier/_projects/personal
mkdir -p ~/.secrets

Then clone all repos as shown above.

dots-quantum & domus-nvim

dots-quantum replaces dotfiles-optimus. The phone has dotfiles-optimus with active symlinks. dots-quantum is not cloned on the phone.

Step 1 — Clone dots-quantum (on phone)
git clone git@github.com:EvanusModestus/dots-quantum.git ~/atelier/_projects/personal/dots-quantum
Step 2 — Enumerate stow packages (on phone)
for pkg in ~/atelier/_projects/personal/dots-quantum/*/; do
  name=$(basename "$pkg")
  [[ "$name" == .* ]] && continue
  files=$(find "$pkg" -type f | wc -l)
  printf "%-25s %4d files\n" "$name" "$files"
done
for f in ~/.zshrc ~/.zshenv ~/.zprofile ~/.gitconfig ~/.fzf ~/.vimrc \
         ~/.vimrc-minimal ~/.vimrc-ide-alt ~/.vimrc-server \
         ~/.fdignore ~/.ripgreprc ~/.bashrc ~/.inputrc; do
  if [ -L "$f" ]; then
    target=$(readlink "$f")
    if [ -e "$f" ]; then
      printf "%-20s → %s\n" "$(basename "$f")" "$target"
    else
      printf "%-20s → %s (BROKEN)\n" "$(basename "$f")" "$target"
    fi
  elif [ -f "$f" ]; then
    printf "%-20s (plain file — stow conflict)\n" "$(basename "$f")"
  else
    printf "%-20s MISSING\n" "$(basename "$f")"
  fi
done
Step 4 — Unstow dotfiles-optimus (on phone)
cd ~/atelier/_projects/personal/dotfiles-optimus/base
stow -D -t ~ zsh git fzf fd ripgrep vim
Step 5 — Remove stow conflicts (on phone)

Plain files block stow — move them out of the way:

mv ~/.zshrc ~/.zshrc.bak
mv ~/.config/omp-theme.toml ~/.config/omp-theme.toml.bak
Step 6 — Stow dots-quantum (on phone)
cd ~/atelier/_projects/personal/dots-quantum
stow -t ~ zsh bash git vim fzf fd ripgrep oh-my-posh shell share bin
for f in ~/.zshrc ~/.zshenv ~/.zprofile ~/.gitconfig ~/.fzf ~/.vimrc \
         ~/.fdignore ~/.ripgreprc ~/.bashrc ~/.inputrc; do
  if [ -L "$f" ]; then
    printf "%-20s → %s\n" "$(basename "$f")" "$(readlink "$f")"
  else
    printf "%-20s NOT SYMLINKED\n" "$(basename "$f")"
  fi
done
Step 8 — Remove dotfiles-optimus and backups (on phone)
rm -rf ~/atelier/_projects/personal/dotfiles-optimus
rm -f ~/.zshrc.bak
rm -f ~/.config/omp-theme.toml.bak
domus-nvim — pull latest (on phone)
git -C ~/.config/nvim pull

Check LSP availability:

for lsp in lua-language-server rust-analyzer pyright typescript-language-server; do
  printf "%-30s " "$lsp"
  command -v "$lsp" 2>/dev/null || echo "MISSING"
done
Repo Drift Check — Process & Command Substitution

Compare workstation vs phone repos using diff <(cmd1) <(cmd2). Process substitution treats command output as a file — no temp files, no pipes.

Compare tracked files (git ls-files)
diff <(git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort) \
     <(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort')

No output = identical. Lines with < are workstation-only, > are phone-only.

Compare all repos at once
REPOS=(
  ~/atelier/_bibliotheca/domus-captures
  ~/atelier/_bibliotheca/domus-infra-ops
  ~/atelier/_bibliotheca/domus-netapi-docs
  ~/atelier/_bibliotheca/domus-secrets-ops
  ~/atelier/_bibliotheca/domus-asciidoc-build
  ~/atelier/_projects/personal/dots-quantum
  ~/.config/nvim
  ~/.secrets
)

for repo in "${REPOS[@]}"; do
  name=$(basename "$repo")
  echo "=== $name ==="
  diff <(git -C "$repo" ls-files 2>/dev/null | sort) \
       <(ssh fold7 "git -C $repo ls-files 2>/dev/null | sort") \
    && echo "IDENTICAL" \
    || true
  echo ""
done
Compare commit history (ahead/behind)
for repo in "${REPOS[@]}"; do
  name=$(basename "$repo")
  local_head=$(git -C "$repo" rev-parse HEAD 2>/dev/null)
  remote_head=$(ssh fold7 "git -C $repo rev-parse HEAD 2>/dev/null")
  if [ "$local_head" = "$remote_head" ]; then
    printf "%-25s SYNCED  %s\n" "$name" "${local_head:0:7}"
  else
    printf "%-25s DRIFTED  local=%s  phone=%s\n" "$name" "${local_head:0:7}" "${remote_head:0:7}"
  fi
done
Compare file counts (quick sanity check)
for repo in "${REPOS[@]}"; do
  name=$(basename "$repo")
  local_count=$(git -C "$repo" ls-files 2>/dev/null | wc -l)
  remote_count=$(ssh fold7 "git -C $repo ls-files 2>/dev/null | wc -l")
  if [ "$local_count" -eq "$remote_count" ]; then
    printf "%-25s OK     %d files\n" "$name" "$local_count"
  else
    printf "%-25s DRIFT  local=%d  phone=%d\n" "$name" "$local_count" "$remote_count"
  fi
done
Compare specific file content
diff <(cat ~/atelier/_bibliotheca/domus-captures/docs/antora.yml) \
     <(ssh fold7 'cat ~/atelier/_bibliotheca/domus-captures/docs/antora.yml')
Process Substitution Patterns

Process substitution <(cmd) creates a temporary file descriptor from command output. The shell expands it to /dev/fd/NN — any tool that reads files can consume it.

# ── diff two commands ────────────────────────────────────
# Compare package lists between two machines
diff <(pacman -Qq | sort) <(ssh fold7 'pkg list-installed 2>/dev/null | awk -F/ "{print \$1}" | sort')

# Compare directory trees
diff <(find ~/atelier -maxdepth 2 -type d | sort) \
     <(ssh fold7 'find ~/atelier -maxdepth 2 -type d | sort')

# Compare SSH config resolution between hosts
diff <(ssh -G kvm-02 | sort) <(ssh fold7 'ssh -G kvm-02 | sort')

# ── paste: side-by-side columns ─────────────────────────
# Local vs remote file counts per repo, side by side
paste <(du -sh ~/atelier/_bibliotheca/*/  2>/dev/null) \
      <(ssh fold7 'du -sh ~/atelier/_bibliotheca/*/ 2>/dev/null')

# ── comm: set operations (requires sorted input) ────────
# Files only on workstation (not on phone)
comm -23 <(git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort) \
         <(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort')

# Files only on phone (not on workstation)
comm -13 <(git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort) \
         <(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort')

# Files on both
comm -12 <(git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort) \
         <(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures ls-files | sort')

# ── wc + paste: count comparison ─────────────────────────
# One-liner file count comparison
paste <(git -C ~/atelier/_bibliotheca/domus-captures ls-files | wc -l) \
      <(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures ls-files | wc -l')

# ── vimdiff: visual ─────────────────────────────────────
# Side-by-side visual diff in neovim
nvim -d <(ssh fold7 'cat ~/.ssh/config') <(cat ~/.ssh/config)
Command Substitution Patterns

Command substitution $(cmd) captures stdout as a string — use it as an argument, variable, or inline value.

# ── variables from commands ──────────────────────────────
PHONE_IP=$(adb shell 'ip addr show wlan0' | awk '/inet / {split($2,a,"/"); print a[1]}')
TERMUX_USER=$(ssh fold7 'whoami')
REPO_COUNT=$(ssh fold7 'find ~/atelier -name .git -type d | wc -l')

echo "Phone: $PHONE_IP  User: $TERMUX_USER  Repos: $REPO_COUNT"

# ── inline in commands ───────────────────────────────────
# Open the file that contains a pattern
nvim $(grep -rl 'vault-ssh-sign' ~/atelier/_bibliotheca/domus-captures/data/)

# cd to the directory of a binary
cd $(dirname $(which vault-ssh-sign))

# SSH to whatever IP the phone currently has
ssh -p 8022 $(adb shell 'ip addr show wlan0' | awk '/inet / {split($2,a,"/"); print a[1]}')

# Git log for the repo containing a specific file
git -C $(dirname $(find ~/atelier -name 'antora.yml' -path '*/domus-captures/*' | head -1)) log --oneline -5

# ── conditional execution ────────────────────────────────
# Install only if missing (idempotent)
command -v yq &>/dev/null || pkg install -y yq

# Clone only if not present
[ -d ~/atelier/_bibliotheca/domus-captures ] || git clone git@github.com:EvanusModestus/domus-captures.git ~/atelier/_bibliotheca/domus-captures

# ── arithmetic ───────────────────────────────────────────
# How many commits ahead is workstation vs phone?
echo "$(git -C ~/atelier/_bibliotheca/domus-captures rev-list --count HEAD) local vs $(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures rev-list --count HEAD') phone"

# ── nested: command sub inside process sub ───────────────
# Diff the SSH keys authorized on two hosts
diff <(ssh kvm-01 'cat ~/.ssh/authorized_keys' | ssh-keygen -lf - 2>/dev/null) \
     <(ssh kvm-02 'cat ~/.ssh/authorized_keys' | ssh-keygen -lf - 2>/dev/null)

# ── heredoc with command sub ─────────────────────────────
# Generate a report with live values
cat <<EOF
Phone Report — $(date +%Y-%m-%d)
IP:       $(adb shell 'ip addr show wlan0' | awk '/inet / {split($2,a,"/"); print a[1]}')
MAC:      $(adb shell 'ip addr show wlan0' | awk '/ether/ {print $2}')
Termux:   $(ssh fold7 'whoami')
Packages: $(ssh fold7 'pkg list-installed 2>/dev/null | wc -l')
Repos:    $(ssh fold7 'find ~/atelier -name .git -type d 2>/dev/null | wc -l')
gopass:   v3=$(ssh fold7 'gopass ls --flat v3 2>/dev/null | wc -l') v4=$(ssh fold7 'gopass ls --flat v4 2>/dev/null | wc -l')
EOF
zsh-Specific Expansions
# ── indirect parameter expansion ─────────────────────────
# ${(P)var} — expand variable name stored in another variable
# Used in tmux env push:
for var in WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE; do
  tmux setenv "$var" "${(P)var}"
done
# Bash equivalent: ${!var}

# ── glob qualifiers ──────────────────────────────────────
# Files modified in last 24h
ls -la ~/atelier/_bibliotheca/domus-captures/**/*.adoc(.m-1)

# Directories only, sorted by modification time
ls -d ~/atelier/**/*(/:om)

# ── parameter expansion flags ────────────────────────────
# Uppercase: ${(U)var}   Lowercase: ${(L)var}
MAC=${(U)$(adb shell 'ip addr show wlan0' | awk '/ether/ {print $2}')}
echo "MAC: $MAC"

# Split string into array: ${(s:/:)var}
path_parts=(${(s:/:)PWD})
echo "Depth: ${#path_parts}"

# ── =() for writing process sub (zsh only) ──────────────
# Creates a temp file that persists until command finishes
# Useful when tool requires a real file path, not /dev/fd
vimdiff =(ssh fold7 'cat ~/.zshrc') ~/.zshrc
Brace Expansion

Not a substitution — the shell expands braces before any command runs. No command output involved, purely syntactic.

# ── sequences ────────────────────────────────────────────
echo {1..10}             # 1 2 3 4 5 6 7 8 9 10
echo {01..05}            # 01 02 03 04 05 (zero-padded)
echo {a..f}              # a b c d e f
echo {1..20..3}          # 1 4 7 10 13 16 19 (step)

# ── multiple items ───────────────────────────────────────
mkdir -p ~/atelier/{_bibliotheca,_projects/personal,_captures,_vaults}

# Create month directories
mkdir -p docs/modules/ROOT/pages/2026/{01..12}

# Backup before editing
cp ~/.ssh/config{,.bak}
# Expands to: cp ~/.ssh/config ~/.ssh/config.bak

# Rename extension
mv report.{txt,adoc}
# Expands to: mv report.txt report.adoc

# ── cartesian product ───────────────────────────────────
echo {vault,kvm,bind}-{01,02}
# vault-01 vault-02 kvm-01 kvm-02 bind-01 bind-02

# SSH health check across cluster
for host in {vault,kvm,bind}-{01,02}; do
  printf "%-15s " "$host"
  ssh "$host" 'uptime -s' 2>/dev/null || echo "UNREACHABLE"
done

# ── diff two configs ─────────────────────────────────────
diff ~/.ssh/config.d/{20-d000-key-hierarchy,80-d000-hosts}.conf
Tilde Expansion
echo ~                    # /home/evanusmodestus
echo ~root                # /root
echo ~+                   # $PWD (current directory)
echo ~-                   # $OLDPWD (previous directory)

# cd back and forth
cd ~/atelier/_bibliotheca/domus-captures
cd ~/atelier/_projects/personal/dots-quantum
cd ~-                     # back to domus-captures
cd ~-                     # back to dots-quantum

# Use in commands
diff ~+/docs/antora.yml ~-/docs/antora.yml
Arithmetic Expansion
# ── basic math ───────────────────────────────────────────
echo $(( 8760 / 24 ))        # 365 — hours to days
echo $(( 1024 * 1024 ))      # 1048576 — KB to bytes

# ── variables ────────────────────────────────────────────
local_count=$(git -C ~/atelier/_bibliotheca/domus-captures ls-files | wc -l)
remote_count=$(ssh fold7 'git -C ~/atelier/_bibliotheca/domus-captures ls-files | wc -l')
echo "Delta: $(( local_count - remote_count )) files"

# ── conditional in arithmetic ────────────────────────────
ttl=8
echo "Cert expires in $(( ttl * 3600 )) seconds"

# ── increment/assign ─────────────────────────────────────
count=0
for repo in ~/atelier/_bibliotheca/*/; do (( count++ )); done
echo "Repos: $count"

# ── base conversion ──────────────────────────────────────
echo $(( 16#FF ))             # 255 — hex to decimal
echo $(( 2#11010 ))           # 26 — binary to decimal
echo $(( 8#755 ))             # 493 — octal to decimal (file perms)
Parameter Expansion (advanced)
# ── defaults ─────────────────────────────────────────────
echo ${PHONE_IP:-10.50.10.145}     # Use default if unset
echo ${EDITOR:=nvim}               # Set AND use default if unset
echo ${VAULT_TOKEN:?Not sourced}   # Error and exit if unset

# ── string manipulation ──────────────────────────────────
file=/home/evanusmodestus/atelier/_bibliotheca/domus-captures/docs/antora.yml
echo ${file##*/}          # antora.yml          (strip longest prefix)
echo ${file%/*}           # .../docs            (strip shortest suffix)
echo ${file%.yml}.json    # ...antora.json      (replace extension)
echo ${file#/home/}       # evanusmodestus/...  (strip prefix)

# ── pattern replacement ──────────────────────────────────
mac="9c:83:06:ce:89:46"
echo ${mac//:/-}          # 9c-83:06:ce:89:46   (first match)
echo ${mac//:/}           # 9c8306ce8946         (all colons removed)
echo ${(U)mac//:/}        # 9C8306CE8946         (zsh: uppercase + strip)

# ── length ───────────────────────────────────────────────
echo ${#mac}              # 17

# ── array slicing (zsh) ─────────────────────────────────
repos=(domus-captures domus-infra-ops dots-quantum domus-nvim secrets)
echo ${repos[1,3]}        # first three
echo ${repos[-1]}         # last element
echo ${#repos}            # count: 5
Filename (Glob) Expansion
# ── basic globs ──────────────────────────────────────────
ls ~/atelier/_bibliotheca/domus-*/docs/antora.yml

# ── recursive (zsh **) ──────────────────────────────────
ls ~/atelier/**/*.adoc(.m-1)      # all .adoc modified in last 24h
ls ~/atelier/**/*.adoc(.L+100k)   # all .adoc larger than 100KB

# ── glob qualifiers (zsh) ───────────────────────────────
ls ~/atelier/**/*(/)              # directories only
ls ~/atelier/**/*(.)              # regular files only
ls ~/atelier/**/*(@)              # symlinks only
ls ~/atelier/**/*(.om[1,5])       # 5 most recently modified files

# ── negation ─────────────────────────────────────────────
ls ~/atelier/_bibliotheca/^domus-captures/     # everything except domus-captures (zsh)

# ── null glob (no error on no match) ────────────────────
setopt nullglob
for f in ~/.ssh/id_*.pub; do echo "$f"; done
# Without nullglob, zsh errors: "no matches found"
History Expansion
!!                        # Repeat last command
sudo !!                   # Re-run last command with sudo
!$                        # Last argument of previous command
!^                        # First argument of previous command
!*                        # All arguments of previous command
!grep                     # Last command starting with "grep"
!?vault?                  # Last command containing "vault"
^old^new                  # Replace "old" with "new" in last command
# Example: ran `ssh kvm-01` → type `^01^02` → runs `ssh kvm-02`

# ── safe preview before executing ────────────────────────
!!:p                      # Print last command without running
!grep:p                   # Print last grep command without running
Quick Reference
Expansion What it does Shell

<(cmd) — Process sub

Command output as readable file descriptor

bash/zsh

>(cmd) — Process sub

Writable file descriptor — pipe into a command

bash/zsh

=(cmd) — Process sub

Command output as temp file (real path)

zsh only

$(cmd) — Command sub

Capture stdout as string

bash/zsh

${var} — Parameter expansion

Variable with modifiers: ##, %%, //, :-, :=, :?

bash/zsh

$expr — Arithmetic

Integer math, base conversion, conditionals

bash/zsh

{a,b,c} — Brace expansion

Cartesian product, sequences, bulk file ops

bash/zsh

~ / ~+ / ~- — Tilde

Home, PWD, OLDPWD

bash/zsh

/ * / ?() — Glob

Filename matching, recursive, qualifiers

bash/zsh

!! / !$ / xy — History

Recall/modify previous commands

bash/zsh

${(P)var} — Indirect

Variable name in a variable

zsh (${!var} bash)

${(U)var} / ${(L)var}

Upper/lowercase string

zsh

Pattern What it does Shell

<(cmd)

Command output as readable file descriptor

bash/zsh

>(cmd)

Writable file descriptor — pipe into a command

bash/zsh

=(cmd)

Command output as temp file (real path, persists)

zsh only

$(cmd)

Capture stdout as string

bash/zsh

${(P)var}

Indirect expansion — variable name in a variable

zsh (${!var} in bash)

${(U)var} / ${(L)var}

Upper/lowercase string

zsh

${var:0:7}

Substring extraction (first 7 chars)

bash/zsh

cmd1 || cmd2

Run cmd2 only if cmd1 fails

bash/zsh

cmd1 && cmd2

Run cmd2 only if cmd1 succeeds

bash/zsh

gopass — v3 Verified, v4 Mount
Current state (2026-07-01)
Item State Action

GPG key

✅ Valid (expires 2027-11-30)

gopass v3

✅ Syncs (confirmed with gopass sync)

gopass v4

❌ Not mounted

Add v4 mount

yq

❌ Not installed

pkg install yq

Install yq (required for v4 extraction)
pkg install yq
yq --version
Clone + mount v4 store

gopass mounts add takes a local path, not a git URL. Clone first, then mount:

git clone git@github.com:EvanusModestus/gopass-v4.git ~/.local/share/gopass/stores/v4
gopass mounts add v4 ~/.local/share/gopass/stores/v4
Do NOT use gopass edit or manually edit gopass config files — it breaks the store. Always use gopass mounts add. And make sure you cd ~ first — gopass interprets relative paths from $PWD.

Sync and verify:

gopass sync
gopass ls v4 | head -20
Verify structured YAML extraction
gopass show v4/domains/d000/identity/ssh/github | yq 'keys'
gopass show v4/domains/d000/identity/ssh/github | yq '.credentials.ssh | keys'
Fix BZIP2 preference (if GPG complains)
gpg --edit-key 28A3183647525597
# setpref AES256 AES192 AES CAST5 3DES SHA512 SHA384 SHA256 SHA224 SHA1 ZIP ZLIB Uncompressed
# save
From-scratch reference (no GPG key at all)

Push GPG key via ADB from workstation:

gpg --export-secret-keys --armor > /tmp/gpg-private.asc
adb push /tmp/gpg-private.asc /sdcard/
rm /tmp/gpg-private.asc

On phone (Termux):

gpg --import ~/storage/shared/gpg-private.asc
rm ~/storage/shared/gpg-private.asc

Then clone and mount both stores:

git clone git@github.com:EvanusModestus/gopass-v3.git ~/.local/share/gopass/stores/v3
gopass mounts add v3 ~/.local/share/gopass/stores/v3
git clone git@github.com:EvanusModestus/gopass-v4.git ~/.local/share/gopass/stores/v4
gopass mounts add v4 ~/.local/share/gopass/stores/v4
gopass sync
Device Registration

Register the Z Fold 7 in SSH config, Vault PKI, and gopass v4. Modeled on the 2026-06-30 android-vault-cert and gopass-v4-standardization workflows.

Step 0 — Discover device identity

From workstation (phone connected via ADB):

adb shell 'ip addr show wlan0' | awk '/inet / {print $2}; /link\/ether/ {print $2}'

Record:

  • IP: 10.50.10.145 (DHCP — will change)

  • MAC: 9c:83:06:ce:89:46

  • Model: SM_F966U1 (Samsung Z Fold 7)

  • ADB serial: R3GYB0J7YHY

Step 1 — Add to SSH config

Add to ~/.ssh/config.d/80-d000-hosts.conf:

cat >> ~/.ssh/config.d/80-d000-hosts.conf << 'EOF'

Host fold7
    HostName 10.50.10.145
    Port 8022
EOF

Verify:

awk '/fold7/,/^$/' ~/.ssh/config.d/80-d000-hosts.conf
DHCP address — set a reservation on VyOS or update the IP after each reconnect. Consider adding a DHCP static mapping:
# On VyOS (configure mode):
# set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 static-mapping zfold7 mac-address 9c:83:06:ce:89:46
# set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 static-mapping zfold7 ip-address 10.50.10.145

Delete stale .age first, then re-encrypt:

rm ~/.ssh/config.d/80-d000-hosts.conf.age
age -e -R ~/.age/recipients/self.txt -o ~/.ssh/config.d/80-d000-hosts.conf.age ~/.ssh/config.d/80-d000-hosts.conf
Step 2 — Source Vault credentials + verify cluster health
ds d000 dev/vault
for node in vault-01 vault-02 vault-03; do
  printf "%-12s " "$node"
  curl -sk "https://$node.inside.domusdigitalis.dev:8200/v1/sys/health" \
    | jq -r '"\(if .sealed then "SEALED" else "unsealed" end)  \(if .standby then "standby" else "ACTIVE" end)  v\(.version)"' \
    2>/dev/null || echo "UNREACHABLE"
done

If any node is sealed, unseal with gopass v4:

gopass show v4/domains/d000/identity/vault/cluster 2>/dev/null \
  | sed -n '/^type:/,$ p' | yq -r '.credentials.unseal."key-1"' | wl-copy
vault operator unseal -address="https://<node>.inside.domusdigitalis.dev:8200"
wl-copy ''

gopass show v4/domains/d000/identity/vault/cluster 2>/dev/null \
  | sed -n '/^type:/,$ p' | yq -r '.credentials.unseal."key-2"' | wl-copy
vault operator unseal -address="https://<node>.inside.domusdigitalis.dev:8200"
wl-copy ''

Verify token:

vault status | grep -E 'Sealed|HA Mode|Active Node'
vault token lookup | awk '/display_name|expire_time|policies/'
Step 3 — Issue EAP-TLS client cert

Verify PKI role:

vault read -format=json pki_int/roles/domus-client \
  | jq '.data | {allowed_domains, organization, ou, key_type, key_bits, max_ttl}'

Export CA chain:

vault read -field=certificate pki/cert/ca > /tmp/vault-root-ca.pem
vault read -field=certificate pki_int/cert/ca > /tmp/vault-intermediate-ca.pem
openssl x509 -in /tmp/vault-root-ca.pem -noout -subject -dates
openssl x509 -in /tmp/vault-intermediate-ca.pem -noout -subject -issuer -dates
openssl verify -CAfile /tmp/vault-root-ca.pem /tmp/vault-intermediate-ca.pem

Issue cert:

vault write -format=json pki_int/issue/domus-client \
  common_name="zfold7.inside.domusdigitalis.dev" \
  ttl="8760h" > /tmp/zfold7-cert.json

Extract and verify:

jq -r '.data.certificate' /tmp/zfold7-cert.json > /tmp/zfold7.pem
jq -r '.data.private_key' /tmp/zfold7-cert.json > /tmp/zfold7-key.pem
jq -r '.data.ca_chain[]' /tmp/zfold7-cert.json > /tmp/zfold7-ca-chain.pem

openssl x509 -in /tmp/zfold7.pem -noout -subject -issuer -dates
openssl verify -CAfile /tmp/vault-root-ca.pem -untrusted /tmp/vault-intermediate-ca.pem /tmp/zfold7.pem

Bundle as PKCS12:

openssl pkcs12 -export \
  -in /tmp/zfold7.pem \
  -inkey /tmp/zfold7-key.pem \
  -certfile /tmp/zfold7-ca-chain.pem \
  -out /tmp/zfold7.p12 \
  -name "Domus EAP-TLS"

Verify bundle:

openssl pkcs12 -in /tmp/zfold7.p12 -info -nokeys 2>/dev/null | grep -E 'subject|issuer'
Step 4 — Push certs to phone
adb push /tmp/vault-root-ca.pem /sdcard/Download/
adb push /tmp/vault-intermediate-ca.pem /sdcard/Download/
adb push /tmp/zfold7.p12 /sdcard/Download/
Step 5 — Install on Android

Install CA certs (root first, then intermediate):

  1. Settings → Security → More security settings → Encryption & credentials

  2. Install a certificate → Certificado de CAvault-root-ca.pem → Name: "Domus Root CA"

  3. Repeat for vault-intermediate-ca.pem → Name: "Domus Intermediate CA"

Install client cert:

  1. Install a certificate → Certificado de Wi-Fizfold7.p12

  2. Enter PKCS12 password

  3. Name: "Domus EAP-TLS"

Do NOT use "Cert. de usuario de aplic. y VPN" — WiFi supplicant does not read from that store.
Step 6 — Disable MAC randomization + configure WiFi
  1. Settings → WiFi → Domus-Secure (gear icon)

  2. Advanced → Tipo de dirección MAC → MAC del teléfono

WiFi profile:

Campo Valor

Red

Domus-Secure

Seguridad

WPA2/WPA3-Enterprise

Método EAP

TLS

Certificado CA

Domus Root CA

Certificado de usuario

Domus EAP-TLS

Identidad

anonymous@inside.domusdigitalis.dev

Identidad anónima

anonymous@inside.domusdigitalis.dev

Dominio

inside.domusdigitalis.dev

Verify real MAC after connecting:

adb shell 'ip addr show wlan0' | awk '/link\/ether/{print $2}'

Samsung OUI starts with 00:, 04:, 14:, 1C:, 20:, 9C:. If you see 66:xx, randomization is still on.

Step 7 — Sign SSH cert for Termux

Get the Termux username first (from Termux on phone: whoami).

ds d000 dev/vault

vault write -field=signed_key ssh/sign/domus-client \
  public_key=@$HOME/.ssh/id_ed25519_d000.pub \
  valid_principals="<termux-user>,evanusmodestus" \
  cert_type=user > /tmp/zfold7-ssh-cert.pub

ssh-keygen -L -f /tmp/zfold7-ssh-cert.pub

Push to phone via ADB (SSH may not be configured yet):

adb push /tmp/zfold7-ssh-cert.pub /sdcard/

On phone (Termux):

cp ~/storage/shared/zfold7-ssh-cert.pub ~/.ssh/id_ed25519-cert.pub
chmod 600 ~/.ssh/id_ed25519-cert.pub
rm ~/storage/shared/zfold7-ssh-cert.pub
Include BOTH the Termux user AND evanusmodestus in valid_principals. INC-2026-03-16 root cause was hardcoded old Termux user (u0_a361).
Step 8 — Create gopass v4 entry

Entry path: v4/domains/d000/network/devices/mobile/zfold7

gopass edit v4/domains/d000/network/devices/mobile/zfold7

Paste this YAML (update IP/Termux user after confirming):

type: "mobile"
hostname: "zfold7"
model: "SM_F966U1"
serial: "R3GYB0J7YHY"
os: "Android + Termux"
mac:
  wifi: "9c:83:06:ce:89:46"
ip: "10.50.10.145"
vlan: "10"
location: "Domus Digitalis"
credentials:
  ssh:
    _use: "Termux sshd — port 8022, ed25519 key"
    port: "8022"
    user: "<termux-user>"
    key_file: "~/.ssh/id_ed25519"
  eap-tls:
    _use: "802.1X WiFi — Vault-issued cert"
    cn: "zfold7.inside.domusdigitalis.dev"
    ca: "Domus Root CA + Intermediate"
    identity: "anonymous@inside.domusdigitalis.dev"
    cert_ttl: "8760h"
network:
  ssid: "Domus-Secure"
  security: "WPA2/WPA3-Enterprise EAP-TLS"
  mac_randomization: "disabled"
pki:
  client_cert_cn: "zfold7.inside.domusdigitalis.dev"
  issuer: "pki_int/issue/domus-client"
  ssh_signer: "ssh/sign/domus-client"
created: "2026-07-01"
updated: "2026-07-01"

Verify:

gps v4/domains/d000/network/devices/mobile/zfold7 'keys'
gps v4/domains/d000/network/devices/mobile/zfold7 '.credentials | keys'
gps v4/domains/d000/network/devices/mobile/zfold7 -r '.hostname + " (" + .ip + ")"'
gps v4/domains/d000/network/devices/mobile/zfold7 -r '.credentials.ssh.port'
gps v4/domains/d000/network/devices/mobile/zfold7 -r '.mac.wifi'
Step 9 — Verify ISE authentication
dsource d000 dev/network/ise
MAC="9C:83:06:CE:89:46"
full-endpoint-profile.sh "$MAC"

ISE Live Logs should show:

  • Authentication method: EAP-TLS

  • Subject CN: zfold7.inside.domusdigitalis.dev

  • VLAN assigned: 10 (Data)

  • Result: PASS

Step 10 — Cleanup
rm -f /tmp/zfold7-cert.json /tmp/zfold7.pem /tmp/zfold7-key.pem \
      /tmp/zfold7-ca-chain.pem /tmp/zfold7.p12 /tmp/zfold7-ssh-cert.pub \
      /tmp/vault-root-ca.pem /tmp/vault-intermediate-ca.pem

Delete from phone after installation:

adb shell 'rm -f /sdcard/Download/vault-root-ca.pem /sdcard/Download/vault-intermediate-ca.pem /sdcard/Download/zfold7.p12'
Private key hygiene

The PKCS12 file contains the private key. After installing on the phone, delete from /sdcard/Download/ immediately. The private key now lives only in Android Keystore (hardware-backed on Samsung Knox).

SSH Session Forensics

Tools for identifying, inspecting, and killing stuck SSH sessions.

lsof — list open files (sockets are files)
# All SSH connections from this machine
lsof -i :22 -i :8022

# SSH connections to a specific host
lsof -i @10.50.10.145

# SSH connections by a specific user
lsof -u evanusmodestus -c ssh

# Mux sockets — SSH ControlMaster files
lsof +D ~/.ssh/sockets/
ss — socket statistics (netstat replacement)
# All established SSH connections
ss -tnp | grep ':22\|:8022'

# Connections to fold7 specifically
ss -tnp dst 10.50.10.145

# Show process info + timer state
ss -tnpo dst 10.50.10.145

# All ESTABLISHED + process name
ss -tnp state established | grep ssh

# Listening sockets (what's accepting connections)
ss -tlnp | grep ssh
fuser — who’s using a port or file
# What process holds port 8022 connections
fuser 8022/tcp

# What process has the mux socket open
fuser ~/.ssh/sockets/*10.50.10.145* 2>/dev/null
Real example — 2026-07-01 fold7 session triage

Three tools, same target — each reveals different detail:

# lsof — PIDs, file descriptors, connection state
lsof -i @10.50.10.145 -n -P
lsof output
COMMAND     PID           USER FD   TYPE   DEVICE SIZE/OFF NODE NAME
ssh     3294350 evanusmodestus 4u  IPv4 22617601      0t0  TCP 10.50.1.204:44758->10.50.10.145:8022 (CLOSE_WAIT)
ssh     3688247 evanusmodestus 3u  IPv4 23534403      0t0  TCP 10.50.1.204:52086->10.50.10.145:8022 (ESTABLISHED)
ssh     3725216 evanusmodestus 3u  IPv4 23627288      0t0  TCP 10.50.1.204:45236->10.50.10.145:8022 (ESTABLISHED)
ssh     3727906 evanusmodestus 3u  IPv4 23627288      0t0  TCP 10.50.1.204:45236->10.50.10.145:8022 (ESTABLISHED)
# ss — socket state, timer info, process mapping
ss -tnpo dst 10.50.10.145
ss output
State      Recv-Q Send-Q  Local Address:Port    Peer Address:Port  Process
CLOSE-WAIT 1      0         10.50.1.204:44758   10.50.10.145:8022  users:(("ssh",pid=3294350,fd=4))
ESTAB      0      0         10.50.1.204:45236   10.50.10.145:8022  users:(("ssh",pid=3727906,fd=3),("ssh",pid=3725216,fd=3))
ESTAB      0      0         10.50.1.204:52086   10.50.10.145:8022  users:(("ssh",pid=3688247,fd=3))
# fuser — just PIDs for a port (local listeners only — empty here because we're the client)
fuser 8022/tcp
Reading the output
PID State Port Analysis

3294350

CLOSE_WAIT

44758

Dead — remote closed, local hasn’t cleaned up. Safe to kill.

3688247

ESTABLISHED

52086

Active standalone session

3725216

ESTABLISHED

45236

Mux master — shares DEVICE 23627288 and port with 3727906

3727906

ESTABLISHED

45236

Mux child — rides on 3725216’s socket (same DEVICE, same port)

Key forensic tells:

  • CLOSE_WAIT — remote side closed (FIN sent), local process hasn’t called close(). Always safe to kill.

  • Same DEVICE number — two PIDs sharing a socket = mux master + child. Kill master and child dies too.

  • Recv-Q > 0 in CLOSE_WAIT — data arrived after remote closed. The session is dead, buffered data is unreadable.

  • fd=3u vs fd=4u3 is the standard SSH socket fd. 4 often means the connection was inherited or reopened (mux).

Surgical kill — target only dead sessions
# lsof approach — kill only CLOSE_WAIT PIDs
lsof -i @10.50.10.145 -n -P | awk '/CLOSE_WAIT/ {print $2, $NF}'
kill $(lsof -i @10.50.10.145 -n -P | awk '/CLOSE_WAIT/ {print $2}')
# ss approach — extract PID from CLOSE_WAIT using awk match() capture group
kill $(ss -tnp dst 10.50.10.145 | awk '/CLOSE-WAIT/ {match($0,/pid=([0-9]+)/,a); print a[1]}')

The awk match($0, /pid=([0-9]+)/, a) captures the PID from ss’s `pid=3294350 format into a[1] — no grep | sed | cut chain needed.

# Verify dead session is gone
lsof -i @10.50.10.145 -n -P | awk '/CLOSE_WAIT/ {print $2}' && echo "STILL ALIVE" || echo "CLEAN"
Graduated kill — full sequence

Always inspect before killing. Never kill -9 first.

# 1. Inspect — what are we about to kill?
lsof -i @10.50.10.145 -n -P

# 2. Kill only dead sessions (CLOSE_WAIT)
kill $(lsof -i @10.50.10.145 -n -P | awk '/CLOSE_WAIT/ {print $2}')

# 3. If killing ALL sessions — graceful SIGTERM first
kill $(lsof -i @10.50.10.145 -t)

# 4. Verify
lsof -i @10.50.10.145 -t && echo "STILL ALIVE" || echo "CLEAN"

# 5. Force only if graceful failed — SIGKILL (no cleanup, mux socket orphaned)
kill -9 $(lsof -i @10.50.10.145 -t) 2>/dev/null

# 6. Clean up orphaned mux sockets after force kill
rm -f ~/.ssh/sockets/*10.50.10.145* 2>/dev/null
Mux-aware kill patterns
# Get PIDs with context — identify master vs child
lsof -i @10.50.10.145 -n -P | awk 'NR>1 {printf "PID %-8s DEVICE %-10s %-12s %s\n", $2, $8, $NF, $9}'

# Kill mux master (children die automatically)
kill $(lsof -i @10.50.10.145 -n -P | awk '/ESTABLISHED/ {print $2}' | sort -u | head -1)

# Kill all except the newest session
kill $(lsof -i @10.50.10.145 -t | sort -n | head -n -1)

# Kill only sessions older than the mux master (stale pre-mux connections)
kill $(lsof -i @10.50.10.145 -n -P | awk '/CLOSE_WAIT/ || /ESTABLISHED/ && $8 != latest_device {print $2}')
Combined audit — one command
echo "=== SSH processes ===" && \
lsof -c ssh -i @10.50.10.145 -n -P 2>/dev/null && \
echo "" && echo "=== Socket state ===" && \
ss -tnpo dst 10.50.10.145 && \
echo "" && echo "=== Mux sockets ===" && \
ls -la ~/.ssh/sockets/*10.50.10.145* 2>/dev/null || echo "none"
SSH escape sequences (inside a stuck session)

If you’re inside a hung SSH session and the terminal is frozen:

~.        # Disconnect (must be first char on a new line — hit Enter first)
~?        # List all escape sequences
~#        # List forwarded connections
~C        # Open command line (add/remove port forwards)
~&        # Background SSH (when waiting for connections to close)
ss vs lsof vs fuser — when to use which
Tool Strength Use when

ss

Fast, kernel-level socket state, timer info, no privilege needed

Quick check — "is there a connection to X?"

lsof

Maps files/sockets to PIDs, full process context, fd details

Need to kill — "which PID owns this connection?"

fuser

Minimal — just PIDs for a port or file

One-liner kill: fuser -k 8022/tcp

Vault SSH cert principals — u0_a385 fix

The workstation’s vault-ssh-sign script uses a fixed principal list. It had u0_a361 (old Termux user) but not u0_a385 (current). Termux sshd maps all connections to the local user (u0_a385), so cert auth fails if that principal is missing.

Re-sign with updated principals:

ds d000 dev/vault
vault write -field=signed_key ssh/sign/domus-client \
  public_key=@$HOME/.ssh/id_ed25519_vault.pub \
  valid_principals="Administrator,admin,adminerosado,ansible,evanusmodestus,gabriel,root,u0_a385" \
  cert_type=user | tee ~/.ssh/id_ed25519_vault-cert.pub > /dev/null

Verify:

ssh-keygen -L -f ~/.ssh/id_ed25519_vault-cert.pub | grep -A 15 'Principals'

Permanent fix — update the script in dots-quantum:

cat ~/.local/bin/vault-ssh-sign
# Find the valid_principals line and add u0_a385, remove u0_a361

Test after re-sign:

# Kill stuck sessions + stale sockets
kill $(lsof -i @10.50.10.145 -t) 2>/dev/null
rm -f ~/.ssh/sockets/*10.50.10.145* 2>/dev/null
ssh -v fold7 2>&1 | grep -E 'Offering|Accepted'
Android UID pattern

Termux user follows Android’s app UID scheme: u0_a[0-9]+

  • u0 — primary Android user (user 0)

  • a — app (always a, never other letters)

  • [0-9]+ — sequentially assigned numeric ID

Changes on app reinstall. Always verify with ssh fold7 'whoami' before signing certs.

Build Tools — ARM64
domus-asciidoc-build

The build-adoc script drives standalone AsciiDoc builds (PDF, HTML, DOCX, EPUB).

ls ~/atelier/_bibliotheca/domus-asciidoc-build/

Check Ruby/Asciidoctor availability on Termux:

pkg install ruby
gem install asciidoctor asciidoctor-pdf asciidoctor-diagram

Symlink or add to PATH:

# Adjust based on where build-adoc lives
ln -sf ~/atelier/_bibliotheca/domus-asciidoc-build/build-adoc ~/bin/build-adoc
Test build
build-adoc $(find ~/atelier/_bibliotheca/domus-captures/data -maxdepth 3 -name '*.adoc' | head -1) html --variant light-cyan
Kroki diagram rendering requires network access to the Kroki server. Local Kroki (Docker) won’t run on Termux — use the public instance or skip diagrams for mobile builds.
Deployment Checklist
Pre-existing (from March 2026 setup)
Check Status

Termux installed (F-Droid)

[x]

Termux:API installed (F-Droid)

[x]

SSH keys (all named) present

[x]

authorized_keys configured

[x]

GPG key imported + valid

[x]

gopass v3 syncs

[x]

oh-my-posh prompt active

[x]

domus-infra-ops cloned

[x]

domus-nvim cloned

[x]

Phone files pulled + cleaned (ADB)

[x]

Completed (2026-07-01)
Check Status

Missing packages installed (neovim, tmux, fzf, fd, rg, yq, wget, go, git-lfs, nmap, netcat)

[x]

Phone cleanup (duplicate nvim clones, stale dirs, starship.toml)

[x]

fold7 added to workstation SSH config (IdentityFile + Port 8022)

[x]

config.d deployed to phone (replaced flat 28KB config with Include)

[x]

Vault SSH cert re-signed (ssh/sign/domus-client, u0_a385 + evanusmodestus principals)

[x]

SSH cert pushed to phone via ADB + installed

[x]

SSH to Vault-trusting host verified from phone (kvm-02)

[x]

dots-quantum cloned + stowed (replaced dotfiles-optimus)

[x]

dotfiles-optimus unstowed + removed

[x]

domus-captures cloned (git-lfs required)

[x]

domus-secrets-ops cloned

[x]

domus-asciidoc-build cloned

[x]

domus-netapi-docs cloned

[x]

~/.secrets cloned

[x]

Stale repos pulled (infra-ops, nvim)

[x]

gopass v4 clone + mount added

[x]

EAP-TLS cert on phone (from 2026-06-30)

[x]

WiFi connected to Domus-Secure (EAP-TLS)

[x]

Remaining
Check Status

80-d000-hosts.conf.age re-encrypted (rm before age -e)

[ ]

domus-captures git-lfs checkout verified (full working tree)

[ ]

Repo drift check (process sub diff workstation vs phone)

[ ]

gopass v4 YAML extraction verified (yq) — YubiKey-bound, read-only on phone

[ ]

gopass v4 entry created (network/devices/mobile/zfold7)

[ ]

DHCP reservation on VyOS

[ ]

Termux:Boot installed + sshd auto-start

[ ]

build-adoc runs on ARM64

[ ]

/tmp cert files cleaned up

[ ]

tmux-hyprland-env

Environment Variables — Linux Quality of Life Reference

Captured 2026-07-01 from P16g (Arch Linux, Hyprland, tmux, domus-nvim). Reference for troubleshooting, new machine setup, and development.

Wayland / Compositor — hyprctl, clipboard, display
Variable Value What Breaks Without It

HYPRLAND_INSTANCE_SIGNATURE

a0136d8c…​ (unique per session)

All hyprctl commands — can’t find compositor socket

WAYLAND_DISPLAY

wayland-1

wl-copy/wl-paste clipboard — copy/paste fails in tmux

DISPLAY

:0

XWayland apps (some Electron apps, xclip, legacy X11 tools)

XDG_RUNTIME_DIR

/run/user/1000

Wayland socket, PipeWire audio, D-Bus, systemctl --user

XDG_SESSION_TYPE

wayland

Session detection — some apps switch behavior (GTK, Qt, Electron)

COLORTERM

truecolor

Terminal 24-bit color — without it, Neovim and bat fall back to 256-color

DBUS_SESSION_BUS_ADDRESS

unix:path=/run/user/1000/bus

Notifications, secrets service (gnome-keyring), polkit prompts

tmux Environment Problem + Fix

tmux server runs independently of the compositor. Panes don’t inherit Hyprland variables unless explicitly pushed.

Push from kitty (where vars are set) — run once after Hyprland starts
for var in WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR; do
  tmux setenv "$var" "${(P)var}"
done

${(P)var} — zsh indirect parameter expansion. Takes the string in $var (e.g. "WAYLAND_DISPLAY") and expands it as a variable name ($WAYLAND_DISPLAY). Bash equivalent: ${!var}.

Pull into existing tmux pane
for var in WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR; do
  eval "$(tmux showenv "$var" 2>/dev/null)"
done
Inspect tmux server environment
tmux showenv                          # all (- prefix = removed)
tmux showenv | grep -v '^-'          # active only
tmux showenv WAYLAND_DISPLAY         # single variable
tmux setenv -u VARNAME               # remove a variable

Variables prefixed with - in tmux showenv (e.g. -SSH_CONNECTION, -WINDOWID) are explicitly removed — tmux strips them so panes don’t inherit stale values.

Editor + Pager — Neovim, bat, man
Variable Value Purpose

EDITOR

nvim

Default editor — git commit, crontab -e, sudoedit, gopass edit

VISUAL

nvim

Visual editor — some tools prefer this over $EDITOR

NVIM_APPNAME

domus-nvim

Neovim config directory — loads ~/.config/domus-nvim/ instead of ~/.config/nvim/

PAGER

less

Default pager — man, git log, git diff

LESS

-R --use-color -Dd+r$Du+b

Less options: raw ANSI (-R), color enabled, red diffs, blue underlines

MANPAGER

sh -c 'col -bx | bat -l man -p'

Man pages rendered through bat — syntax highlighting + line numbers

MANROFFOPT

-c

Suppress SGR sequences for bat compatibility

GIT_EDITOR

true

Suppresses interactive editor in git — used when committing via heredoc/script

GPG_TTY

/dev/pts/21

GPG signing needs the current TTY — without it, gpg --sign hangs

Search + Fuzzy Find — fzf, ripgrep, fd
Variable Value Purpose

FZF_DEFAULT_COMMAND

rg --files --hidden --follow --smart-case --glob "!.git" …​

What fzf searches by default — ripgrep respects .gitignore

FZF_CTRL_T_COMMAND

Same as default

Ctrl+T file picker in shell

FZF_ALT_C_COMMAND

fd --type d --hidden --follow --exclude .git

Alt+C directory jumper — fd for speed

RIPGREP_CONFIG_PATH

~/.ripgreprc

Persistent ripgrep config — default flags, ignore patterns

GREP_PATTERNS

~/.local/share/grep-patterns

Custom grep pattern library

FZF_DEFAULT_OPTS

Tokyo Night color scheme, keybindings, preview window

Consistent fzf appearance across all invocations

FZF keybindings worth knowing: Ctrl+A select all, Ctrl+D deselect, Ctrl+G accept, Ctrl+/ toggle preview, Ctrl+Y copy to clipboard, Enter opens in $EDITOR.

Paths + Project Structure
Variable Value Purpose

ATELIER

~/atelier

Root of all projects — everything lives under this

DOMUS_CAPTURES

~/atelier/_bibliotheca/domus-captures

This repo

DOMUS_DOCS

~/atelier/_bibliotheca/domus-docs

Antora hub

DOTFILES

~/atelier/_projects/personal/dots-quantum

GNU Stow managed dotfiles

AWKPATH

~/.local/share/awk:/usr/share/awk

Custom awk libraries — @include resolves here

SED_SCRIPTS

~/.local/share/sed

Reusable sed scripts

BASH_LIB_DIR

~/.local/share/bash

Shared bash library functions

KUBECONFIG

~/.kube/config

Kubernetes cluster config — k3s lab

Containers + Build
Variable Value Purpose

DOCKER_BUILDKIT

1

BuildKit backend — parallel builds, cache mounts, secrets

COMPOSE_DOCKER_CLI_BUILD

1

Docker Compose uses BuildKit

NODE_ENV

development

Node.js mode — verbose errors, no minification

TERRAFORM_WORKSPACE

default

Active Terraform workspace

Privacy + Telemetry
Variable Purpose

DISABLE_TELEMETRY=1

Blanket telemetry opt-out (respected by many tools)

DISABLE_ERROR_REPORTING=1

Crash report opt-out

DISABLE_BUG_COMMAND=1

Auto bug-report opt-out

CONDA_PROMPT_MODIFIER=false

Suppress conda prompt clutter

VIRTUAL_ENV_DISABLE_PROMPT=1

Suppress venv prompt — oh-my-posh handles it

PYENV_VIRTUALENV_DISABLE_PROMPT=1

Same for pyenv

Ollama (Local LLM)
Variable Purpose

OLLAMA_FLASH_ATTENTION=1

Flash attention — faster inference, lower VRAM

OLLAMA_KV_CACHE_TYPE=q8_0

8-bit quantized KV cache — saves VRAM

OLLAMA_MAX_LOADED_MODELS=3

Keep 3 models warm in memory

OLLAMA_NUM_PARALLEL=4

Parallel inference requests

PATH Order (left to right = highest priority)
~/.local/bin                    ← User scripts (highest priority)
~/.local/share/gem/ruby/3.4.0  ← Ruby gems (asciidoctor, etc.)
~/.cargo/bin                    ← Rust tools (ripgrep, fd, bat, etc.)
~/go/bin                        ← Go tools
~/.npm-global/bin               ← Node.js global packages
~/.secrets/bin                  ← dsec, encrypt-file, decrypt-file
~/.opencode/bin                 ← OpenCode AI tools
~/atelier/.../dots-quantum/bin  ← Domus custom scripts
~/tools                         ← Standalone binaries
/usr/local/bin                  ← System-wide local installs
/usr/bin                        ← System packages (pacman)
Laptop Panel Black After Sleep — Root Cause + Fix
Root Cause

hypridle sets brightness to 0 on timeout/lid close. On resume, brightnessctl -r tries to restore but fails silently because the backlight sysfs file is root:root 0644 — user can’t write to it.

# The chain:
on-timeout  → brightnessctl -s set 0     ← saves + dims (works initially)
on-resume   → brightnessctl -r            ← restore fails — permission denied, silent
lid close   → brightnessctl -s set 0      ← same
lid open    → brightnessctl -r            ← same silent failure
# Current permissions — user has no write access
-rw-r--r-- root root /sys/class/backlight/intel_backlight/brightness
Fix — udev Rule + Video Group
# 1. Add user to video group
sudo usermod -aG video evanusmodestus

# 2. Create udev rule — sets backlight group to video with g+w on boot
echo 'ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chgrp video /sys/class/backlight/%k/brightness", RUN+="/bin/chmod g+w /sys/class/backlight/%k/brightness"' \
  | sudo tee /etc/udev/rules.d/90-backlight.rules

Reboot required — group membership only applies to new login sessions.

Verify After Reboot
# Confirm group membership
groups | grep video

# Confirm permissions changed
ls -la /sys/class/backlight/intel_backlight/brightness
# Should show: -rw-rw-r-- root video

# Confirm brightnessctl works without sudo
brightnessctl set 50%
brightnessctl set 100%
Hyprland Config Reference

From ~/.config/hypr/hypridle.conf:

# Timeout dimming (saves + restores)
on-timeout = brightnessctl -s set 10%
on-resume = brightnessctl -r

# Deep sleep — full blackout
on-timeout = brightnessctl -s set 0
on-resume = brightnessctl -r

From ~/.config/hypr/hyprland.conf:

# Lid switch — lock + blackout / restore
bindl = , switch:on:Lid Switch, exec, loginctl lock-session && brightnessctl -s set 0
bindl = , switch:off:Lid Switch, exec, brightnessctl -r

# Manual brightness keys
bindle = , XF86MonBrightnessUp, exec, brightnessctl set +5%
bindle = , XF86MonBrightnessDown, exec, brightnessctl set 5%-

# Sleep resume — re-enable monitor
after_sleep_cmd = hyprctl keyword monitor "eDP-1, preferred, auto, 2" && hyprctl dispatch dpms on

All of these depend on brightnessctl having write access to the backlight device.

Emergency Recovery (Before udev Fix Is Applied)
# Quick fix — needs sudo
sudo brightnessctl set 50%

# Hyprland DPMS toggle (if compositor issue, not backlight)
hyprctl dispatch dpms off eDP-1 && sleep 1 && hyprctl dispatch dpms on eDP-1

# Force mode reset
hyprctl keyword monitor eDP-1,3200x2000@60,0x0,2
hyprctl dispatch focusmonitor eDP-1
Monitor Status Check
hyprctl monitors -j | jq -r '.[] | "\(.name)  dpms=\(.dpmsStatus)  focused=\(.focused)  \(.width)x\(.height)@\(.refreshRate)"'
brightnessctl info

Steam Installation — AppArmor Hardened

Steam Installation — AppArmor Hardened

Pacman-based Steam install on modestus-p16g (Arch, RTX 5090, AppArmor). Replaces the default unconfined AppArmor profile with actual confinement — denies access to credential stores (~/.ssh, ~/.gnupg, ~/.age, gopass) while allowing GPU, network, and game directories. Includes clean uninstall procedure and GMod Lua mods for programming with Gabriel.

Prerequisites

Current system state on modestus-p16g:

Component Status

GPU

RTX 5090 — nvidia-open 610.43.02, lib32-nvidia-utils installed

Vulkan

vulkan-icd-loader + lib32-vulkan-icd-loader both present. No conflicting drivers (no lib32-vulkan-intel — clean NVIDIA-only stack).

multilib

Enabled in /etc/pacman.conf

Locale

en_US.UTF-8 generated (required by Steam — prevents invalid pointer errors)

vm.max_map_count

1048576 (already set — some games crash without this; see Arch Wiki)

AppArmor

Module loaded, service active, 4.1.7-1

kernel.unprivileged_userns_clone

Was 0 (disabled) — must be 1 for Steam. Steam requires unprivileged user namespaces for its sandbox. Set live and persist below.

Existing Steam profile

/etc/apparmor.d/steamunconfined placeholder (replaced below)

Steam

Installed 2026-07-01 (32 packages)

Arch Wiki prerequisites checklist (source)

The Wiki lists these requirements. Status on this machine:

  • multilib enabled

  • 32-bit OpenGL/Vulkan drivers (lib32-nvidia-utils, lib32-vulkan-icd-loader)

  • en_US.UTF-8 locale generated

  • vm.max_map_count >= 1048576

  • Steam package installed

  • No conflicting Vulkan drivers (only NVIDIA — no Intel/AMD lib32 conflict)

  • kernel.unprivileged_userns_clone = 1 (set below)

Enable unprivileged user namespaces (scoped, not persistent)

Steam’s sandbox requires unprivileged user namespaces. This machine intentionally disables them via /etc/sysctl.d/99-hardening.conf (container escape mitigation). Rather than undo that hardening permanently, we use a wrapper script that enables the sysctl only while Steam is running.

Why not a persistent drop-in?

A persistent 80-steam.conf would be overridden by 99-hardening.conf anyway — sysctl drop-ins load in lexicographic order and the last value wins. Editing 99-hardening.conf would weaken your hardening permanently. The wrapper script is the correct approach: minimum exposure window, no persistent changes, self-reverting.

~/.local/bin/steam-safe — wrapper script
#!/bin/bash
# steam-safe — launch Steam with unprivileged user namespaces enabled,
#              disable them again on exit.
#
# Why: Steam requires kernel.unprivileged_userns_clone=1 for its sandbox.
#      This machine disables it by default (99-hardening.conf) because
#      user namespaces have been a privilege escalation vector:
#        - CVE-2022-0185: heap overflow via user namespace
#        - CVE-2023-2640: Ubuntu OverlayFS user namespace escalation
#      This script limits the exposure window to the Steam session only.

set -euo pipefail

enable_userns() { sudo sysctl -q -w kernel.unprivileged_userns_clone=1; }
disable_userns() { sudo sysctl -q -w kernel.unprivileged_userns_clone=0; }

# Ensure we disable on exit, even if Steam crashes
trap disable_userns EXIT

echo "[steam-safe] Enabling unprivileged user namespaces..."
enable_userns

echo "[steam-safe] Launching Steam..."
/usr/bin/steam "$@"

# trap EXIT handles disable_userns
echo "[steam-safe] Steam exited. User namespaces disabled."

Install the wrapper:

# Create the script
cat > ~/.local/bin/steam-safe << 'SCRIPT'
#!/bin/bash
set -euo pipefail
enable_userns() { sudo sysctl -q -w kernel.unprivileged_userns_clone=1; }
disable_userns() { sudo sysctl -q -w kernel.unprivileged_userns_clone=0; }
trap disable_userns EXIT
echo "[steam-safe] Enabling unprivileged user namespaces..."
enable_userns
echo "[steam-safe] Launching Steam..."
/usr/bin/steam "$@"
echo "[steam-safe] Steam exited. User namespaces disabled."
SCRIPT

# Make executable
chmod +x ~/.local/bin/steam-safe

# Verify
which steam-safe && steam-safe --help 2>&1 | head -3

From now on, launch Steam with steam-safe instead of steam. On exit (clean or crash), the trap EXIT reverts the sysctl automatically.

Security note: unprivileged user namespaces

unprivileged_userns_clone=1 allows any user to create user namespaces without root. User namespaces have historically been a privilege escalation vector (CVE-2022-0185, CVE-2023-2640). The wrapper limits exposure to the Steam session window only. Between sessions, the hardening in 99-hardening.conf remains in effect.

AppArmor’s userns permission in the Steam profile provides a second layer: even while the sysctl is enabled, only profiles with explicit userns grants can create namespaces. Other confined processes remain restricted.

Teaching Gabriel: defense in depth

This is a real-world example of defense in depth — multiple independent layers of security:

  1. Sysctl (kernel.unprivileged_userns_clone) — the system-wide gate. Normally off.

  2. Wrapper script — opens the gate only when needed, closes it on exit.

  3. AppArmor profile — even when the gate is open, only Steam is allowed through.

  4. trap EXIT — ensures cleanup happens even if Steam crashes. This is a Bash pattern worth learning: always clean up after yourself, especially for security-sensitive state.

No single layer is perfect. Together, they reduce the attack window from "always open" to "open only during gameplay, and only for Steam."

Verify before proceeding
# Confirm multilib is enabled
grep -A1 '^\[multilib\]' /etc/pacman.conf

# Confirm NVIDIA driver stack + Vulkan (no conflicting drivers)
pacman -Q nvidia-open nvidia-utils lib32-nvidia-utils vulkan-icd-loader lib32-vulkan-icd-loader

# Confirm locale
locale -a | grep -i en_US

# Confirm vm.max_map_count
sysctl vm.max_map_count

# Confirm unprivileged user namespaces
sysctl kernel.unprivileged_userns_clone

# Confirm AppArmor is active
systemctl is-active apparmor && aa-enabled
Installation
Install Steam
# Before — confirm not installed
pacman -Q steam 2>/dev/null && echo "Already installed" || echo "Ready to install"

# Install Steam (lib32-vulkan-icd-loader already present)
sudo pacman -S steam

# After — verify
pacman -Q steam
Why the steam package and not Flatpak or steam-native-runtime?

The Arch Wiki recommends the steam package (pacman) as the primary install path. Two alternatives exist:

  • Flatpak — sandboxed but adds its own isolation layer (complicates AppArmor, library debugging, and addon deployment). Useful if you need patched glibc for Easy Anti-Cheat, but GMod doesn’t need that.

  • steam-native-runtime (AUR) — replaces Steam’s bundled Ubuntu runtime with system libraries. The Wiki explicitly warns against this: "not recommended as it might break some games due to binary incompatibility." It pulls 130+ dependencies and creates fragile library coupling.

The steam package uses Steam’s own runtime (~/.steam/root/ubuntu12_32/steam-runtime/) with LD_PRELOAD overrides for known-broken libraries. This is the supported path.

Vulkan driver note

The Arch Wiki warns that pacman may prompt for a 32-bit Vulkan driver during Steam install and alphabetically defaults to lib32-nvidia-utils — which can break Vulkan on non-NVIDIA systems. On this machine, lib32-nvidia-utils is correct (RTX 5090). If you ever install on an Intel/AMD machine, watch this prompt carefully.

First launch

Run Steam once before applying AppArmor — it needs to bootstrap its runtime:

# Launch Steam, log in, let it update, then close
steam

This creates ~/.local/share/Steam/ — the Steam runtime directory. AppArmor paths below reference this directory, so it must exist first.

Package inventory for clean uninstall

32 packages installed as dependencies on 2026-07-01:

Details
alsa-plugins-1:1.2.12-5          lib32-alsa-lib-1.2.16.1-1
lib32-alsa-plugins-1.2.12-1      lib32-dbus-1.16.2-1
lib32-fontconfig-2:2.18.1-1      lib32-freetype2-2.14.3-1
lib32-glib2-2.88.1-1             lib32-libgcrypt-1.12.2-1
lib32-libgpg-error-1.61-1        lib32-libnm-1.56.1-1
lib32-libpipewire-1:1.6.7-1      lib32-libpng-1.6.58-1
lib32-libva-2.22.0-1             lib32-libxcrypt-compat-4.5.2-1
lib32-libxfixes-6.0.1-2          lib32-libxinerama-1.1.6-1
lib32-libxss-1.2.5-1             lib32-nspr-4.39-1
lib32-nss-3.125-1                lib32-pcre2-10.47-1
lib32-pipewire-1:1.6.7-1         lib32-sqlite-3.53.2-1
lib32-systemd-260.2-1            lib32-util-linux-2.42.2-1
libxcrypt-compat-4.5.2-1         lsb-release-2.0.r55.a25a4fc-1
steam-devices-1.0.0.85-7         xdg-user-dirs-0.20-1
xorg-xrandr-1.5.4-1              zenity-4.2.2-1
lib32-vulkan-icd-loader-1.4.350.0-1  steam-1.0.0.85-7
# After Steam first launch, record home directory footprint
du -sh ~/.local/share/Steam/ ~/.steam/
AppArmor Hardening

The default /etc/apparmor.d/steam profile ships unconfined — it names the binary but enforces nothing. Replace it with an actual confinement profile.

Why confine Steam?

Steam runs arbitrary third-party code (game binaries, workshop addons, Proton/Wine layers). It has:

  • Write access to your entire $HOME by default

  • Network access (multiplayer, workshop downloads, telemetry)

  • Access to input devices, GPU, audio

Confinement limits blast radius: a malicious workshop addon or compromised game binary can’t read ~/.ssh/, ~/.gnupg/, gopass stores, or .age identities.

Profile

Back up the existing profile before replacing:

sudo cp /etc/apparmor.d/steam /etc/apparmor.d/steam.unconfined.bak
/etc/apparmor.d/steam — enforcing profile
# AppArmor profile for Steam (Arch Linux, pacman install)
# Replaces the default unconfined placeholder.
# Strategy: allow Steam's own directories + GPU/audio/input + network.
#           Deny access to sensitive dotfiles and credential stores.

abi <abi/4.0>,
include <tunables/global>

profile steam /usr/{lib/steam/bin_steam.sh,games/steam} flags=(complain) { (1)
  include <abstractions/base>
  include <abstractions/fonts>
  include <abstractions/nameservice>
  include <abstractions/dbus-session-strict>
  include <abstractions/audio>
  include <abstractions/vulkan>
  include <abstractions/mesa>

  # ── User namespaces (Steam requires this) ──
  userns,

  # ── Network (multiplayer, downloads, auth) ──
  network inet stream,
  network inet dgram,
  network inet6 stream,
  network inet6 dgram,
  network unix stream,
  network unix dgram,

  # ── Steam's own directories ──
  owner @{HOME}/.local/share/Steam/** rwkl,
  owner @{HOME}/.steam/** rwkl,
  owner @{HOME}/.steampath r,
  owner @{HOME}/.steampid r,

  # ── Game content (GMod addons, saves, configs) ──
  owner @{HOME}/.local/share/Garry's Mod/** rwk,

  # ── Steam runtime and Proton ──
  /usr/lib/steam/** rmix,
  /usr/share/steam/** r,
  /tmp/.X11-unix/** rw,
  /tmp/steam* rwk,
  /tmp/dumps/** rwk,
  /tmp/proton_* rwk,
  owner /tmp/vdpau-drivers-* rw,

  # ── GPU access (NVIDIA) ──
  /dev/nvidia* rw,
  /dev/dri/** rw,
  /dev/shm/** rwk,
  /proc/driver/nvidia/** r,
  /sys/devices/pci**/{vendor,device,class,config,resource*} r,
  /sys/class/drm/** r,
  /usr/lib/libGL*.so* rm,
  /usr/lib/libEGL*.so* rm,
  /usr/lib/libvulkan*.so* rm,
  /usr/lib/libnvidia*.so* rm,
  /usr/lib32/lib*.so* rm,

  # ── System reads Steam needs ──
  /etc/machine-id r,
  /etc/hostname r,
  /etc/hosts r,
  /etc/resolv.conf r,
  /etc/nsswitch.conf r,
  /etc/ld.so.cache r,
  /etc/ld.so.preload r,
  /proc/ r,
  /proc/** r,
  /sys/devices/system/cpu/** r,
  /sys/fs/cgroup/** r,
  owner /proc/*/fd/ r,
  owner /proc/*/maps r,

  # ── Input devices (controllers, keyboard for games) ──
  /dev/input/** rw,
  /dev/uinput rw,
  /run/udev/data/** r,
  /sys/class/input/** r,

  # ── Shared libraries ──
  /usr/lib/** rm,
  /usr/lib32/** rm,

  # ── XDG desktop integration ──
  owner @{HOME}/.config/unity3d/** rwk,
  owner @{HOME}/.config/cef_user_data/** rwk,

  # ── Deny sensitive paths explicitly ── (2)
  deny @{HOME}/.ssh/** rwklx,
  deny @{HOME}/.gnupg/** rwklx,
  deny @{HOME}/.age/** rwklx,
  deny @{HOME}/.config/gopass/** rwklx,
  deny @{HOME}/.local/share/gopass/** rwklx,
  deny @{HOME}/.secrets/** rwklx,
  deny @{HOME}/.claude/** rwklx,
  deny @{HOME}/.aws/** rwklx,
  deny @{HOME}/.kube/** rwklx,
  deny @{HOME}/.vault-token rwklx,
  deny @{HOME}/.netrc rwklx,
  deny @{HOME}/.gitconfig rwklx,

  # Site-specific overrides
  include if exists <local/steam>
}
1 Starts in complain mode — logs violations without blocking. Switch to enforce after testing.
2 Explicit denies take precedence even if a broader allow would match. These protect credential stores, secrets, and identity material.
Load and verify
# Parse the profile for syntax errors
sudo apparmor_parser -Q /etc/apparmor.d/steam

# Load the profile
sudo apparmor_parser -r /etc/apparmor.d/steam

# Verify it loaded in complain mode
sudo aa-status | grep steam
Complain-to-enforce workflow

Run Steam in complain mode first. Play GMod, install workshop addons, test multiplayer. Then review what was logged:

# Check for AppArmor denials (complain mode logs as ALLOWED but flags them)
sudo dmesg | grep -i 'apparmor.*steam' | tail -20

# Or use the audit log
sudo journalctl -k | grep -i 'apparmor.*steam' | tail -20

If no unexpected denials appear after a session or two, switch to enforce:

# Edit the profile: change flags=(complain) to flags=(enforce)
sudo sed -i 's/flags=(complain)/flags=(enforce)/' /etc/apparmor.d/steam

# Verify the change
awk '/^profile steam/' /etc/apparmor.d/steam

# Reload
sudo apparmor_parser -r /etc/apparmor.d/steam
sudo aa-status | grep steam
Teaching point: complain vs enforce

AppArmor has two active modes. Complain logs policy violations to the kernel ring buffer but allows them — it’s an observation mode. Enforce blocks violations and logs them. Always deploy new profiles in complain first; a wrong deny in enforce mode will silently break Steam with cryptic errors. The kernel log (dmesg) is your feedback loop.

Clean Uninstall

Full reversal — leaves no orphaned packages, no residual config, no dangling AppArmor profile, no security relaxation.

Step 1: Remove Steam package and orphaned dependencies
# Remove Steam and any dependencies that nothing else needs
sudo pacman -Rns steam

# Check if lib32-vulkan-icd-loader is still needed by anything else
pactree -r lib32-vulkan-icd-loader
# If only Steam needed it:
sudo pacman -Rns lib32-vulkan-icd-loader
Why -Rns instead of -R?

-R removes the package. -Rs also removes dependencies that were pulled in only for this package (recursive). -Rns adds config file removal (-n). Together, this prevents orphaned packages and stale configs from accumulating. Always use -Rns for clean removal unless you know a dependency is shared.

Step 2: Remove steam-safe wrapper
rm ~/.local/bin/steam-safe

No sysctl drop-in to remove — the wrapper enabled user namespaces only during runtime. 99-hardening.conf was never modified, so kernel.unprivileged_userns_clone=0 remains enforced on boot and between sessions.

Step 3: Remove user data
# See what Steam left behind
du -sh ~/.local/share/Steam/ ~/.steam/ ~/.config/unity3d/ 2>/dev/null

# Remove it
rm -rf ~/.local/share/Steam/ ~/.steam/ ~/.steampath ~/.steampid
rm -rf ~/.config/unity3d/
rm -rf ~/.config/cef_user_data/

# Remove desktop entries Steam may have created
rm -f ~/.local/share/applications/steam*.desktop
rm -f ~/.local/share/icons/hicolor/*/apps/steam*.png 2>/dev/null

# Verify nothing remains
find ~ -maxdepth 3 -iname '*steam*' -not -path '*/.local/share/Trash/*' 2>/dev/null
Step 4: Remove AppArmor profile
# Unload the profile from the kernel
sudo apparmor_parser -R /etc/apparmor.d/steam

# Remove the profile file and backup
sudo rm /etc/apparmor.d/steam /etc/apparmor.d/steam.unconfined.bak

# Remove local overrides if any were created
sudo rm -f /etc/apparmor.d/local/steam

# Verify
sudo aa-status | grep steam  # should return nothing
Step 5: Verify clean state
# No steam packages
pacman -Q | grep -i steam

# No steam processes
pgrep -a steam

# No steam files in home
find ~ -maxdepth 3 -iname '*steam*' 2>/dev/null

# No apparmor profile
ls /etc/apparmor.d/steam 2>/dev/null

# User namespaces still hardened
sysctl kernel.unprivileged_userns_clone  # should be 0

# No wrapper script
which steam-safe 2>/dev/null  # should return nothing
Troubleshooting

Common issues from the Arch Wiki, filtered for this setup (NVIDIA, Arch, pacman install).

Steam runtime library conflicts

Steam bundles its own Ubuntu-based runtime libraries. If a game crashes at launch, the bundled library may conflict with a system library.

# Check what Steam is loading from its runtime vs system
for i in $(pgrep steam); do
  awk '/\.local/{print $NF}' /proc/$i/maps
done | sort -u

# If a game crashes, check its specific library requirements
ldd ~/.steam/root/steamapps/common/<GAME>/game_executable 2>&1 | grep 'not found'
The LD_PRELOAD override mechanism

The /usr/bin/steam launcher overrides specific runtime libraries known to cause problems via LD_PRELOAD. This is why you should always launch via steam (the wrapper) rather than /usr/lib/steam/steam (the raw script). The wrapper patches known incompatibilities at load time.

Game crashes with DxvkError (Vulkan conflict)

If a game crashes with terminate called after throwing an instance of 'dxvk::DxvkError', conflicting Vulkan drivers are installed. Verify only NVIDIA Vulkan drivers are present:

# Should show ONLY nvidia-related vulkan packages, no intel/amd
pacman -Q | grep -i vulkan
# Expected: vulkan-icd-loader, lib32-vulkan-icd-loader (loaders only — drivers come from nvidia-utils)
Games freeze at start when focused (NVIDIA + Xorg)

Known issue with ForceFullCompositionPipeline + specific NVIDIA driver versions under Xorg. The game runs in the background but freezes when focused.

Fix: disable ForceFullCompositionPipeline or use Wayland (Hyprland on this machine avoids this entirely).

Debugging output

Steam no longer redirects stdout/stderr to /tmp/dumps/ by default. Launch from terminal to capture output:

# Capture Steam output for debugging
steam 2>&1 | tee /tmp/steam-debug-$(date +%F).log
AppArmor denials breaking Steam

If Steam or a game misbehaves after switching to enforce mode, check the kernel log:

# Real-time AppArmor denial monitoring
sudo dmesg -w | grep --line-buffered 'apparmor.*DENIED'

# Or check journal
sudo journalctl -k --since "1 hour ago" | grep -i 'apparmor.*steam'

To temporarily switch back to complain for debugging:

sudo aa-complain /etc/apparmor.d/steam
# Test Steam, then re-enforce:
sudo aa-enforce /etc/apparmor.d/steam
GMod Lua Curriculum

Building on the existing HUD addon and 6-weapon arsenal deployed on 2026-06-28 (see concern gabriel-lua-gmod), these mods are designed to teach programming concepts through gameplay. Each mod is a self-contained lesson — deploy one per session, talk through the code, then let him experiment.

Progression map
Phase Mod Concepts taught Real-world parallel

1

Chat Commands

String parsing, if/else, tables

CLI argument parsing

2

NPC Spawner

Functions, parameters, vectors, loops

API calls with arguments

3

Inventory System

Tables as data structures, save/load, serialization

Database CRUD

4

Quest System

State machines, event hooks, callbacks

Event-driven architecture

5

Admin Panel (VGUI)

GUI programming, layout, user input

Frontend development

6

Networked Game Mode

Client/server model, networking, sync

Distributed systems

Phase 1: Chat Commands

A chat command processor — type !spawn zombie 5 in chat and it spawns 5 zombies. Teaches string splitting, conditionals, and the hook system.

lua/autorun/server/gabriel_commands.lua
-- GABRIEL'S COMMAND SYSTEM
-- Type !help in chat to see all commands

local COMMANDS = {} (1)

-- Register a command: name, function, description
local function RegisterCommand(name, fn, desc) (2)
    COMMANDS[name] = {run = fn, help = desc}
end

-- ── Commands ──

RegisterCommand("spawn", function(ply, args) (3)
    local npcType = args[1] or "npc_zombie"
    local count = tonumber(args[2]) or 1
    count = math.Clamp(count, 1, 10) (4)

    for i = 1, count do
        local npc = ents.Create(npcType)
        if IsValid(npc) then
            local pos = ply:GetEyeTrace().HitPos + Vector(0, 0, 10) (5)
                      + Vector(math.random(-50, 50), math.random(-50, 50), 0)
            npc:SetPos(pos)
            npc:Spawn()
        end
    end
    ply:ChatPrint("[CMD] Spawned " .. count .. "x " .. npcType)
end, "!spawn <npc_type> <count> — spawn NPCs where you look")

RegisterCommand("god", function(ply, args)
    if ply:HasGodMode() then
        ply:GodDisable()
        ply:ChatPrint("[CMD] God mode OFF")
    else
        ply:GodEnable()
        ply:ChatPrint("[CMD] God mode ON")
    end
end, "!god — toggle god mode")

RegisterCommand("speed", function(ply, args)
    local mult = tonumber(args[1]) or 2
    mult = math.Clamp(mult, 0.5, 10)
    ply:SetRunSpeed(500 * mult) (6)
    ply:SetWalkSpeed(200 * mult)
    ply:ChatPrint("[CMD] Speed set to " .. mult .. "x")
end, "!speed <multiplier> — change move speed")

RegisterCommand("cleanup", function(ply, args)
    local removed = 0
    for _, ent in ipairs(ents.GetAll()) do
        if ent:IsNPC() then
            ent:Remove()
            removed = removed + 1
        end
    end
    ply:ChatPrint("[CMD] Removed " .. removed .. " NPCs")
end, "!cleanup — remove all NPCs")

RegisterCommand("help", function(ply, args)
    ply:ChatPrint("── Gabriel's Commands ──")
    for name, cmd in SortedPairs(COMMANDS) do
        ply:ChatPrint("  " .. cmd.help)
    end
end, "!help — show this list")

-- ── The Hook — this connects chat to our commands ── (7)

hook.Add("PlayerSay", "GabrielCommands", function(ply, text)
    if text:sub(1, 1) ~= "!" then return end (8)

    local parts = string.Explode(" ", text) (9)
    local cmdName = parts[1]:sub(2):lower()  -- strip the "!" and lowercase
    table.remove(parts, 1)                   -- remaining parts are arguments

    local cmd = COMMANDS[cmdName]
    if cmd then
        cmd.run(ply, parts)
        return "" (10)
    else
        ply:ChatPrint("[CMD] Unknown command. Type !help")
    end
end)

print("[GABRIEL] Command system loaded! Type !help in chat.")
1 A table used as a dictionary — keys are command names, values are command objects. This is how real programs store registrations (routes, CLI subcommands, plugin hooks).
2 A function that takes parameters. name is a string, fn is a function (functions are values in Lua — you can store them in tables, pass them around). desc is documentation.
3 ply is the player who typed the command. args is a table of the remaining words. Real CLI tools work the same way: git commit -m "msg" → command=commit, args={"-m", "msg"}.
4 math.Clamp prevents abuse — can’t spawn 10,000 NPCs. This is input validation, the same concept that prevents SQL injection in web apps.
5 GetEyeTrace().HitPos — raycasting. Shoots an invisible line from your eyes forward and returns where it hits the world. Used in games, physics engines, and 3D modeling.
6 Default walk speed is 200, run is 500. We multiply both so the ratio stays consistent. This is proportional scaling — common in UI layout, signal processing, and game balance.
7 Hooks are GMod’s event system. PlayerSay fires every time someone types in chat. We register a callback function that GMod calls when the event fires. This pattern — register a function to be called later — is foundational: JavaScript event listeners, Python decorators, webhook handlers.
8 Early return pattern: if the message doesn’t start with !, bail immediately. Keeps the logic flat instead of nesting everything in an if. Senior engineers call this a guard clause.
9 string.Explode(" ", text) splits "!spawn zombie 5" into {"!spawn", "zombie", "5"}. Same as Python’s str.split() or Bash’s word splitting.
10 Returning "" suppresses the message from chat — other players don’t see your commands. Returning nil (or nothing) would show it normally. This is message interception.
Challenges
  1. Add !tp <player> — teleport to another player (ply:SetPos(target:GetPos()))

  2. Add !give <weapon> — combine with the arsenal: ply:Give("gabriel_rifle")

  3. Add !explodeutil.BlastDamage(ply, ply, ply:GetPos(), 200, 100) (you’ll die too — that’s the lesson)

  4. Add !noclipply:SetMoveType(MOVETYPE_NOCLIP) to fly through walls

  5. Hardest: Add !alias <name> <command> — lets you create shortcuts. Requires storing aliases in another table.

Phase 2: NPC Spawner with AI Behaviors

A spawn menu that creates NPCs with custom behaviors — patrol routes, follow the player, attack on sight. Teaches functions with parameters and spatial math.

lua/autorun/server/gabriel_npc_spawner.lua
-- GABRIEL'S NPC SPAWNER
-- Spawns NPCs with custom AI behaviors

local function SpawnFollower(ply) (1)
    local npc = ents.Create("npc_citizen")
    if not IsValid(npc) then return end

    npc:SetPos(ply:GetEyeTrace().HitPos + Vector(0, 0, 10))
    npc:SetKeyValue("spawnflags", "256") -- long visibility, won't go far
    npc:Spawn()
    npc:Activate()

    -- Make it follow the player
    npc:SetSchedule(SCHED_TARGET_CHASE) (2)
    npc:SetTarget(ply)

    -- Give it a name tag
    npc.GabrielName = "Follower #" .. math.random(100, 999)

    -- Reacquire target every 3 seconds (3)
    timer.Create("follow_" .. npc:EntIndex(), 3, 0, function()
        if IsValid(npc) and IsValid(ply) then
            npc:SetTarget(ply)
            npc:SetSchedule(SCHED_TARGET_CHASE)
        else
            timer.Remove("follow_" .. npc:EntIndex())
        end
    end)

    ply:ChatPrint("[NPC] " .. npc.GabrielName .. " is now following you!")
    return npc
end

local function SpawnPatroller(ply) (4)
    local npc = ents.Create("npc_combine_s")
    if not IsValid(npc) then return end

    local origin = ply:GetEyeTrace().HitPos + Vector(0, 0, 10)
    npc:SetPos(origin)
    npc:Give("weapon_smg1")
    npc:Spawn()
    npc:Activate()

    npc.GabrielName = "Patrol #" .. math.random(100, 999)
    npc.PatrolPoints = {} (5)

    -- Generate a square patrol route around spawn point
    local radius = 300
    local points = {
        origin + Vector(radius, 0, 0),
        origin + Vector(0, radius, 0),
        origin + Vector(-radius, 0, 0),
        origin + Vector(0, -radius, 0),
    }

    local currentPoint = 1
    timer.Create("patrol_" .. npc:EntIndex(), 4, 0, function()
        if not IsValid(npc) then
            timer.Remove("patrol_" .. npc:EntIndex())
            return
        end
        npc:SetLastPosition(points[currentPoint]) (6)
        npc:SetSchedule(SCHED_FORCED_GO_RUN)
        currentPoint = currentPoint % #points + 1 (7)
    end)

    ply:ChatPrint("[NPC] " .. npc.GabrielName .. " patrolling (radius " .. radius .. ")")
    return npc
end

-- Wire into the command system if it exists
hook.Add("PlayerSay", "GabrielNPCSpawner", function(ply, text)
    if text == "!follow" then
        SpawnFollower(ply)
        return ""
    elseif text == "!patrol" then
        SpawnPatroller(ply)
        return ""
    end
end)

print("[GABRIEL] NPC Spawner loaded! Commands: !follow, !patrol")
1 Functions that return something — SpawnFollower creates an NPC and returns it. The caller can use the return value to do more with it. This is how APIs work: you call a function, it does work, it gives you back a result.
2 Schedules are GMod’s AI behavior system. SCHED_TARGET_CHASE means "move toward your target." The NPC doesn’t know how to navigate — the engine’s pathfinding handles that. You just declare what you want. This is declarative programming.
3 timer.Create runs a function repeatedly (every 3 seconds, forever — 0 means infinite repeats). The function inside checks if the NPC and player still exist (IsValid) before acting. This is defensive programming — never assume something still exists. Real servers do the same: check if a database connection is alive before querying.
4 Same pattern, different behavior. Both functions create NPCs but configure them differently. This is polymorphism at its simplest — same interface, different behavior.
5 Empty table assigned to the NPC as a custom field. Lua lets you add any field to any table at runtime. This is both powerful and dangerous — no compiler catches typos.
6 SetLastPosition + SCHED_FORCED_GO_RUN = "run to this exact point." We cycle through 4 corners of a square. The NPC walks a patrol route.
7 Modular arithmetic for cycling: 1 → 2 → 3 → 4 → 1 → 2 → …​. The % operator wraps around. This pattern appears everywhere: circular buffers, round-robin schedulers, clock arithmetic.
Challenges
  1. Change the patrol shape — triangle (3 points), hexagon (6 points), circle (math.sin/math.cos with angles)

  2. Make followers say thingsnpc:EmitSound("vo/npc/male01/hi01.wav") when they get close

  3. Add !army 5 — spawn 5 followers at once using a for loop

  4. Add !fight — spawn 2 opposing NPC teams, watch them battle

Ad-Hoc Requests

Ad-Hoc Requests

Capture walk-ups, Teams pings, and unplanned work here.

*


Work (CHLA)

CHARGE TIME IN PEOPLESOFT - CRITICAL. Do this NOW before anything else.

Critical (P0)

Project Description Owner Status Due Blocker

Linux Research (Xianming Ding)

EAP-TLS for Linux workstations, dACL, UFW

Evan

BEHIND (72 days overdue)

02-24

Certificate "password required" - nmcli fix documented

iPSK Manager

Pre-shared key automation

Ben Castillo

BEHIND

 — 

DB replication issues

MSCHAPv2 Migration

Legacy auth deprecation — 6,227 devices, 5 waves. 6 batch SQL queries + 3-API endpoint profile script added (05-11). Report due.

Evan

25% — Report due, batch queries ready

05-30

Report to turn in

Research Segmentation

All endpoints to Untrusted VLAN

Evan

BLOCKED

 — 

CISO decision pending

Disaster Recovery

ISE DR scoping — dot1x closed mode = total blackout

Evan

Scoping

 — 

 — 

Mandiant Remediation

Copy 4/16 findings, Guest ACL lab, Q2 assessment

Evan

Active

 — 

 — 

SIEM QRadar → Sentinel

Full SIEM platform transition. Monad console error resolved 05-12. Secrets configured. Blocked on DCR creation (Rule ID + Stream Name). Azure private network policy unresolved.

Evan

Active — blocked on DCR

Q2 2026

Victor/Mauricio: create DCR, resolve Azure network policy

Abnormal Security

AI email platform — ESA cutover. CR assigned, CAB May 12 15:00. Implementation May 14 10:00.

Evan

Active — CAB today 15:00

05-14

Pre-CAB checklist: confirm Tyler, Jason, Sarah

High Priority (P1)

Project Description Owner Status Target

ISE 3.4 Migration

Upgrade from 3.2p9

Evan

Blocked — maintenance window needed

Q2 2026

Switch Upgrades

IOS-XE fleet update (C9300, 3560CX)

Evan

Pending

Q2 2026

Spikewell BYOD VPN

dACL SQL, AD group integration

Evan

Active

 — 

Strongline Gateway

MAC capture, Identity Group setup — 37 days aging

Evan

Active — David Rukiza assigned

 — 

Abnormal Security

AI email security platform research, ESA cutover timeline

Evan

Newly assigned

 — 

DMZ Migration

External services audit behind NetScaler

Evan

Audit phase

 — 

Firewall Audit (murus-portae)

EtherChannel query, prefilter, policy assignments

Evan

Scoping — ASA API creds needed

 — 

iPSK Manager HA

Server 2 config, TLS, SQL security audit

Evan

In progress

 — 

Sentinel KQL

Build proficiency, distinguish from team

Evan

Onboarding

 — 

VNC Blocking

Block and eliminate VNC protocol enterprise-wide

Evan

Active — Phase 0 (Discovery)

Mid-June 2026

Strategic (P2)

Project Description Owner Status

HHS Regulatory Compliance

New HHS security policies implementation

TBD

NOT STARTED

InfoSec Reporting Dashboard

PowerBI metrics for executives

TBD

NOT STARTED

EDR Migration (AMP → Defender)

Endpoint protection consolidation

TBD

NOT STARTED

Azure Legacy Migration

Modern landing zone

Team

In Progress

ChromeOS EAP-TLS

SCEP + Victor, Paul testing

Victor

In Progress

P0 — Critical / Blocking

Security & Compliance

  • ISE 3.2 Patch 10 upgrade — CVE-2026-20147 CVSS 9.9 / CVE-2026-20148. Propose maintenance window once patch confirmed on software.cisco.com.

  • ISE Advisory sa-ise-rce-traversal-8bYndVrZ — check Patch 10 availability

  • Mandiant Remediation — findings status tracked. Working session prep + defensive posture documented (comms-2026-04-24). Copy 4/16 updates into Excel at work. Guest ACL lockdown (WIR-M-01) pending lab validation. appendix-todos updated with MSCHAPv2 milestones.

  • Guest ACL update — guest redirect ACL work needed. Lab validate GUEST_CWA_REDIRECT_MAX_SECURITY in d000, then joint CR with NE. On today’s task list.

  • Disaster Recovery & Downtime Procedures — ISE top priority (dot1x closed mode = SPOF for network access)

    • ISE DR: Document failover sequence — PAN, MnT, PSN priority order

    • ISE DR: RADIUS dead-server detection on WLCs/switches — critical-auth VLAN fallback

    • ISE DR: Backup/restore procedures — scheduled config backups, tested restores

    • FTD/FMC DR: FMC loss = no policy management

    • Network DR: Core/distribution switch failure, STP reconvergence, HSRP failover

    • Document RTO/RPO per system

SIEM Migration (QRadar → Sentinel)

  • SIEM QRadar → Sentinel Migration — LEAD ROLE. 4 collection iterations (Apr 16, 17, 17-streamlined, 20-streamlined). Python chart pipeline built (qradar-charts.py). Migration XLSX generated. Verification pending. Comms sent Apr 23.

    • d001 artifacts: 8 JSON exports, 2 CSV inventories, migration XLSX, top5 source SVG/PNG, verification doc

    • Dependency: Monad pipeline for log source transition

    • Dependency: Sentinel KQL proficiency for query migration

  • Monad Pipeline Evaluation (origin: 2026-03-11) — lead role. Console error RESOLVED 05-12. 06-09: Architecture decision — rsyslog (CHLXSYSLOG01) as collection tier → Monad → Sentinel. ISE lab → rsyslog → Monad 6-step execution guide created with 10 API calls. ASA lab logs already flowing through rsyslog. DCR still needed — Victor + Mauricio.

  • Sentinel KQL — build proficiency, distinguish from team. Azure portal access acquired.

  • QRadar log source report — run AQL queries, fetch JSON, generate Python Excel

Active Deployments & Migrations

  • MSCHAPv2 Migration — 6-sheet Standard Report ready. Migration window 05-04 to 05-30 CLOSED. Confirm final report status and next steps with team. 6,227 MSCHAPv2 devices, 14,249 EAP-TLS/TEAP (70% migrated).

  • MSCHAPv2 weekly cadence — recurring Wednesday call established (first 04-22). Completed 2026-04-22.

  • MSCHAPv2 ownership matrix — sent in scoping email 4/24 with manager callouts (@Albert, @John). Completed 2026-04-24.

  • TCP Clocks deployment — Batch 1: 7 clocks validated (OUI 40:AC:8D). Batch 2 (06-09): 9 new MACs (OUI 40:AC:BD) added via ERS. 1 one-off reassigned. New switch deployed without RADIUS/AAA — clocks can’t authenticate. Switch onboarding template + 3 validation queries documented. ERS queries self-contained with ers function.

  • SRT Research VLAN — confirm roles with Tony Sun: Tony implementor, Evan tester. CAB approved 04-21.

  • Downtime Computers enforcement — draft ISE AuthZ rule: medigate_724 + Wireless = DenyAccess. Separate CR. d001: DC queries, audit CSVs (v1-v3), wireless violations report delivered 04-21.

  • Enterprise Linux 802.1X — standardize Shahab/Ding deployment (CISO priority). Overdue since 02-24. Blocked by nmcli cert fix. 06-15: Assembler build fixed (26 errors → 0). Segmentation document extension to 06-16. d001 open linux-research to work on it.

  • Abnormal Security — CR-2026-05-07. Implemented 05-13. 06-09 update: Full policy review — 20-section EOP validation commands rebuilt, Hoxhunt SCL-1 investigation (intentional bypass confirmed), sclizer junk folder triage (~800 emails), Outlook reactions audit added, Connect-ExchangeOnline msalruntime fix documented. ESA migration expansion in progress — priority to move off ESA to full environment.

    • Team: Cox/William, Landeros/Jason, Rosado/Evan, Naranjo/Mauricio, Sandoval/Carlos

  • ASA VPN: Okta RADIUS → Entra SAML — (NEW 06-09) 5-phase migration plan built. ASA baseline captured (2 tunnel groups: CHLA_CORPORATE_USERS, CHLA_BYOD_USERS). 6 ISE policy screenshots. Tony Sun (ASA), Justin Halbmann (Entra/Okta), Evan (ISE). VPN cert expires 07-28. PDF deliverable ready. Share with team this week.

Tube System Upgrade (NEW — 06-01)

  • Tube System Upgrade — iTrack 3528165. 15x 10" TS stations need MAC addresses added to ISE identity group IoT_Onboard. MACs received from vendor (C8:1A:FE:20:xx:xx series). Station list spans ICU (CTICU, PICU, BMT, NICU, NICCU), ED, Surgery, Trauma, Pharmacy. Vendor contact: John Genest. Rationale: manufacturer no longer supports current system; failure risks delayed/missed patient care.

BMS Controller Segmentation (MIGRATED — 06-09)

  • BMS Controller Segmentation — Full migration from Principia LaTeX to d001. 12 partials, 5 Mermaid diagrams, 4 legacy PDFs, ISE screenshots. d001 open bms-controller. Completed 2026-06-09.

BMS Device Inventory (NEW — 04-24)

  • BMS Device Inventory — 72 devices discovered across 37 switches (04-24). Profile-driven architecture (Claroty/Medigate). 16 queries built. Phase 0 complete. Next: cross-reference with Visio diagrams, classify by function, begin D2 diagrams. Cleanup: delete 4 orphaned test groups, migrate 4 retire-dACL devices, investigate 3 null-profile devices.

VNC Blocking (NEW — 05-11)

  • VNC Blocking — block and eliminate VNC enterprise-wide. Due mid-June 2026. Phase 0: discovery. January AQL query baseline to incorporate. Cross-reference BMS inventory for VNC-capable devices.

Investigations & Audits

  • Murus Portae (WAF) — Phase 0 discovery in progress. FMC cert expired. d001: DMZ NetScaler WAF investigation, zone map, architecture D2 diagrams (v1+v2 SVGs), FMC REST API reference guide, ops script. FMC API returning zero ACP rules — under investigation.

  • Firewall audit — FMC discovery inventory done (d001: fmc-discovery-2026-04-16). EtherChannel query, prefilter, policy assignments pending.

  • IoT Dr. Kim devices — RECURRING. All 4 MACs validated in IoT_iPSK_VLAN1620_Misc (04-24). v2 validation queries built with 7 deep analysis queries (group flapping, credential leakage, profile drift, NAS tracking, remediation timeline, deny audit, OUI scan). Revalidate — confirm no flapping since 04-24.

  • IoT device validation queries — v2 created with partials architecture, 16 queries across ERS/MnT/DataConnect/FMC. Completed 2026-04-24.

Stale Blockers (carried via carryover tracker)

  • k3s NAT verification — rule 170, 10.42.0.0/16 pod network (origin: 2026-03-09). 92 days. Blocks Wazuh indexer recovery → blocks SIEM visibility. Decide: test or defer to Q3.

  • Strongline Gateway VLAN fix — 8 devices wrong identity group (origin: 2026-03-16). 85 days. David Rukiza assigned — follow up on status.

Administrative

  • PeopleSoft — track time for current week

  • iTrack tickets — close open tickets

  • KQL library — build initial queries in codex + d001

  • Linux Research project — finalize and review

  • Tax filing 2025 (MFJ) — see encrypted case file in data/d000/personal/ for details and action items

P1 — Important

  • MSCHAPv2 action-item tracker — owner/status/next-steps per workstream

  • ISE admin MFA enforcement — recommendation tied to advisory (interim control pending Patch 10)

  • DMZ Migration — external services audit behind NetScaler. Linked to Murus Portae investigation.

  • Vocera/Wyse iTrack RCA — complete root cause report

  • GCC ISE Support — 3/4 nodes restored, PSN-04 deferred

  • Wazuh indexer recovery — blocked by k3s NAT (origin: 2026-03-09)

  • Vocera EAP-TLS Supplicant Fix (origin: 2026-03-12)

  • iPSK Manager HA — blocked by DB replication (Ben Castillo)

  • ISE 3.4 Migration — depends on Patch 10 completion first

  • Git history scrub — murus-portae-output.md + ise-analytics CSVs

  • Encrypt prep-cmds-2026-04-15.adoc — plaintext committed to git

  • ISE MnT Messaging Service — enable UDP syslog delivery (maintenance window needed)

Infrastructure (Personal)

  • Borg backups — test and validate on ALL systems (Razer, P16g, vault-01, bind-01, kvm-01, kvm-02)

  • Borg — verify backup script paths updated from dotfiles-optimus to dots-quantum

  • Borg — create initial archive for ThinkPad P16g if none exists

  • Libvirt VLAN hook debug on both KVMs

  • Te1/0/2 cable replacement and re-test

  • Vault Raft cluster — verify vault-01 rejoined

  • Fix EAP-TLS keyring/secrets issue on Razer workstation

Completed (confirmed — do not delete, archive only)

  • CR-2026-04-15 SRT Research VLAN — submitted to iTrack. Completed 2026-04-15.

  • CAB presentation 4/21 — SRT Research VLAN 233 → CHLA-Research. APPROVED. Completed 2026-04-21.

  • Downtime Computers wireless audit — 45 computers, 16 violating, v3 report delivered. Completed 2026-04-21.

  • Git identity fix — dots-quantum/git/.gitconfig email corrected. Completed 2026-04-21.

  • MSCHAPv2 10:30 meeting — next steps + ACL coordination. Completed 2026-04-17.

Service Requests (SR)

SR# Request Requestor Opened Status

3508542

Zoll cards connection issue

STALE — verify in iTrack

3508524

Disable dot1x on (2) network ports - 5th floor 3250 Wilshire (PXE-boot imaging issues)

STALE — verify in iTrack (issues persisted after disable)

3528165

Tube System Upgrade — 15 stations, MAC addresses for ISE IoT_Onboard identity group

Genest, John (vendor contact)

2026-06-01

NEW — MACs received, need ISE onboarding

Incidents (INC)

INC# Priority Description Opened SLA Status

1911859

Strongline Gateways in Miscellaneous Subnet

STALE — verify in iTrack (related to carryover P0)

Change Requests - Emergency (ECAB)

CR# Description Opened Scheduled Status

No emergency changes

Change Requests - Normal

CR# Description Opened Scheduled Status

No normal changes

Change Requests - Scheduled/Standard

CR# Description Opened Window Status

No scheduled changes

Change Requests - Root Cause / Post-Incident

CR# Description Related INC Opened Status

100451

Vocera Phones and Wyse devices went off network

STALE — verify in iTrack


Session Accomplishments (Claude Code)

Day-specific accomplishments here.


Personal

In Progress

Project Description Status Notes

k3s Platform

Production k3s cluster on kvm-01

Active

Prometheus, Grafana, Wazuh deployed

Wazuh Archives

Enable archives indexing in Filebeat

Active

PVC fix pending

kvm-02 Hardware

Supermicro B deployment

Active

Hardware ready, RAM upgrade done

Planned

Project Description Target Blocked By

Vault HA (3-node)

vault-02, vault-03 on kvm-02

Q2 2026 (slipped from Q1)

kvm-02 deployment

k3s HA (3-node)

Control plane HA

Q2 2026 (slipped from Q1)

kvm-02 deployment

ArgoCD GitOps

k3s GitOps deployment

After k3s stable

 — 

MinIO S3

Object storage for k3s

After ArgoCD

 — 

Domus Inventory

Personal asset management (YAML + CLI + AsciiDoc)

Q2 2026

Schema approved

Active — Infrastructure

Task Details Priority Status Due

Wazuh agent deployment

Deploy agents to all infrastructure hosts

P2

Pending

After archives fix

k3s Platform

Production k3s cluster on kvm-01

P1

In Progress

 — 

Wazuh Archives

Enable archives indexing in Filebeat, PVC fix

P1

In Progress

 — 

kvm-02 Hardware

Supermicro B deployment, RAM upgrade done

P1

In Progress

 — 


Active — Security & Encryption

Task Details Priority Status Due

Configure 4th YubiKey

SSH FIDO2 keys

P1

TODO

 — 

Cold storage M-DISC backup

age-encrypted archives

P1

TODO

After YubiKey setup


Active — Development & Tools

Task Details Priority Status Due

netapi Commercialization

Go CLI rewrite with Cobra-style argument discovery, package for distribution

P0

Active

 — 

Ollama API Service

FastAPI (17 endpoints), productize — config audit, doc tools, runbook gen

P0

Active

 — 

Shell functions (fe, fec, fef)

File hunting helpers

P3

TODO

 — 


Active — Documentation

Task Details Priority Status Due

D2 Catppuccin Mocha styling

domus-* spoke repos (177 files total)

P3

In Progress

 — 


Active — Financial

Task Details Priority Status Due

Amazon order history import

Download CSV from Privacy Central → parse with awk → populate subscriptions tracker

P1

Waiting

Pending Amazon data export (requested 2026-04-04)


Active — Education

Task Details Priority Status Due

No active education tasks — see education trackers


Active — Personal & Life Admin

Task Details Priority Status Due

ThinkPad T16g Setup

Arch install, stow dotfiles, Ollama stack, netapi dev env

P0

Pending

 — 

P50 Arch to Ubuntu migration

CR-2026-03-12

P2

In Progress

 — 

X1 Carbon Ubuntu installs

2 laptops, LUKS encryption

P2

In Progress

 — 

P50 Steam Test

Test Flatpak Steam + apt cleanup of broken i386 packages

P3

Pending

 — 

Documentation Sites

Notes

Day-specific personal notes here.


Education

Claude Code Mastery

Resource Details Progress Status

Claude Code Full Course (4 hrs)

Nick Saraev - YouTube comprehensive course

26:49 / 4:00:00

IN PROGRESS

Claude Code Certification

Anthropic official certification (newly released)

Not started

GOAL

Skills Mastery (Critical)

Certification Deadlines

  • CISSP - July 12, 2026 (10-week plan active — Week 1)

  • RHCSA 9 - Q3 2026 (after CISSP)

  • LPIC-1 - Renewal required (blocks LPIC-2)

Spanish C1 Certification Goals

Certification Provider Target Status Strategy

SIELE C1

Instituto Cervantes / UNAM / Salamanca

Q2 2026

ACTIVE

Computer-based, faster results - take FIRST

DELE C1

Instituto Cervantes

Q3/Q4 2026

PLANNED

After SIELE success, harder exam

DELE C2

Instituto Cervantes

2027

FUTURE

Mastery level - requires extensive immersion

SIELE is computer-adaptive, results in 3 weeks. DELE is paper-based, results in 3-4 months. Do SIELE first to validate readiness.

Don Quijote Writing Practice - DELE C1/C2 Initiative

Method:

  1. Read chapter in original Spanish

  2. Write personal analysis/understanding en espanol

  3. AI review for grammar, vocabulary, register

  4. Build comprehensive understanding of literary elements

Today’s Study

  • Focus: CISSP (41 days to July 12 exam — schedule exam today 06-01), MSCHAPv2 migration wrap-up

  • Secondary: RHCSA curriculum, Spanish SIELE C1

  • CISSP — Security & Risk Management (continuing). Schedule exam this afternoon.

  • RHCSA — continue curriculum phase

  • Spanish — Don Quijote reading + analysis (DTLA study day)

  • MSCHAPv2 — migration window closed 05-30, review final report

Regex Training (CRITICAL)

  • Status: 52 days carried over (since 2026-03-16)

  • Priority: After PeopleSoft, before Quijote

  • Session: Character classes, word boundaries


Infrastructure

Documentation Sites

Site URL Status Actions Needed

Domus Digitalis

docs.domusdigitalis.dev

Active

Validate, harden, improve

Architectus

docs.architectus.dev

Active

Public portfolio site - maintain

HA Deployment Status

System Description Status Notes

VyOS HA

vyos-01 (kvm-01) + vyos-02 (kvm-02) with VRRP VIP

✅ COMPLETE

2026-03-07 - pfSense decommissioned

BIND DNS HA

bind-01 (kvm-01) + bind-02 (kvm-02) with AXFR

✅ COMPLETE

Zone transfer operational

Vault HA

Raft cluster (vault-01/02/03)

✅ COMPLETE

Integrated with PKI

Keycloak Rebuild

keycloak-01 corrupted, rebuild from scratch

🔄 NEXT

Priority P3 - SSO broken

FreeIPA HA

ipa-02 replica planned

📋 PLANNED

Linux auth redundancy

AD DC HA

home-dc02 replication

📋 PLANNED

Windows auth redundancy

iPSK Manager HA

ipsk-mgr-02 with MySQL replication

📋 PLANNED

PSK portal redundancy

ISE HA

PAN HA (ise-01 reconfigure)

⏳ DEFERRED

Wait until ise-02 stable

ISE 3.5 Migration

Upgrade path: 3.2p9 → 3.4 (P1) → 3.5 (target)

📋 PLANNED

After 3.4 Migration completes (Q2 2026)

Single Points of Failure (CRITICAL)

These systems have NO redundancy - outage impacts production.
System Impact if Down Mitigation

ISE (ise-02)

All 802.1X stops - wired and wireless auth fails

ise-01 reconfiguration deferred until ise-02 stable

Keycloak (keycloak-01)

SAML/OIDC SSO broken (ISE admin, Grafana, etc.)

NEXT PRIORITY - Rebuild runbook

FreeIPA (ipa-01)

Linux auth, sudo rules, HBAC fails

ipa-02 replica planned

AD DC (home-dc01)

Windows auth, Kerberos, GPO fails

home-dc02 replica planned

iPSK Manager

Self-service PSK portal unavailable

ipsk-mgr-02 with MySQL replication planned

Validation Tasks

Task Details Status

docs.domusdigitalis.dev validation

Test all cross-references, search, rendering

TODO

docs.domusdigitalis.dev hardening

HTTPS, CSP headers, security review

TODO

docs.architectus.dev validation

Public site content review

TODO

Hub-spoke sync verification

All components building correctly

Ongoing


Quick Commands

Git & GitHub CLI

create GitHub repo from existing local repo
gh repo create <name> --private --source . --remote origin --push
clone a forked repo into a specific directory
gh repo clone EvanusModestus/PowerShell ~/atelier/_projects/work/PowerShell
gh repo clone defaults to SSH. If key is passphrase-protected, load agent first: eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_github
cross-repo commit search — all domus repos on a specific date
for repo in ~/atelier/_bibliotheca/domus-*/ ~/atelier/_projects/personal/domus-*/; do
  [ -d "$repo/.git" ] || continue
  name=$(basename "$repo")
  git -C "$repo" log --since="2026-04-06" --until="2026-04-07" --format="%h %aI %s" 2>/dev/null |
    awk -v r="$name" '{print r, $0}'
done
commit history touching only today’s modified files
git log --oneline -- $(find . -name "*.adoc" -type f -newermt "$(date +%F)")
unstage a file without losing changes
git restore --staged data/d001/api/ise-dataconnect/output/output-2026-04-24

Safe — removes from staging area only. Working tree is untouched. Use when you accidentally git add a plaintext or output file.

gh CLI — repo discovery and filtering

list repos by name pattern (domus/antora ecosystem)
gh repo list --limit 100 --json name,description \
  | jq -r '.[] | select(.name | test("domus|antora|asciidoc"; "i")) | "\(.name)\t\(.description)"'
top 20 most recently updated repos
gh repo list --limit 100 --json name,description,updatedAt \
  | jq -r 'sort_by(.updatedAt) | reverse | .[:20] | .[] | "\(.updatedAt[:10])\t\(.name)\t\(.description)"'
top 10 repos by disk usage
gh repo list --limit 100 --json name,diskUsage \
  | jq -r '.[] | "\(.diskUsage)\t\(.name)"' | sort -rn | head -10
clone a repo that’s not local yet
gh repo clone EvanusModestus/<repo-name> ~/atelier/_bibliotheca/<repo-name>

find & grep

files modified since midnight today (precise — not "last 24 hours")
find . -name "*.adoc" -type f -newermt "$(date +%F)" | sort
-mtime 0 means "last 24 hours", not "today". -newermt "$(date +%F)" compares against midnight — exact.
case-insensitive file search
find . -iname "*mschap*" -type f | sort
multiple name patterns with -o
find . -type f \( -iname "*ise*" -o -iname "*mschap*" \) | sort
same thing, single regex — fewer parens, extensible
find . -type f -iregex '.*\(ise\|mschap\).*'
exclude directories
find . -type f -iname "*meeting*" \
  -not -path "*/node_modules/*" \
  -not -path "*/.git/*" \
  -not -path "*/build/*"
recent drafts by modification time (newest first)
find .drafts -type f -printf '%T@ %Tc %p\n' | sort -rn | awk '{$1="";print}' | head -3
grep — know what you’re counting
grep -rl "pattern" . --include="*.adoc"         # file count (which files)
grep -rn "pattern" . --include="*.adoc"         # line matches (every occurrence)
grep -rc "pattern" . --include="*.adoc" | grep -v ':0$'  # match count per file
search with context — avoid opening the file
grep -rn -E 'git init|gh repo create' docs/ --include='*.adoc' -B2 -A2

Search codex by content — which files contain a command?

find all PowerShell files that use a specific cmdlet
find docs/modules/ROOT/examples/codex/powershell -type f -name "*.adoc" \
  -exec grep -l 'Get-Process\|Start-Process\|pipeline\|Where-Object' {} \;

Pattern: find -exec grep -l returns only filenames with matches — like grep -rl but with find’s `-type f -name filtering. Use \| for OR in grep basic regex. Swap the pattern for any cmdlet or keyword to locate coverage across the codex.

inventory a codex tool directory — count files per tier
find docs/modules/ROOT -name "powershell" -type d \
  -exec sh -c 'echo "$1: $(find "$1" -type f | wc -l) files"' _ {} \;
find orphaned examples (not included by any page)
for f in $(find docs/modules/ROOT/examples/codex/powershell -name "*.adoc" -type f); do
  base=$(basename "$f")
  dir_parent=$(basename $(dirname "$f"))
  grep -rq "$dir_parent/$base" docs/modules/ROOT/pages/codex/powershell/ \
    docs/modules/ROOT/examples/codex/powershell/*.adoc 2>/dev/null \
    || echo "ORPHAN: $f"
done

find → grep → open in nvim

find by path + content, open result in nvim
nvim $(find -path '*oauth*' -name '*.adoc' -type f \
  -exec grep -l 'timeout\|expire\|reconfig\|token' {} \;)

Command substitution $(…​) feeds all matches as arguments to nvim — opens every hit as a buffer. :bn/:bp to cycle, :ls to list. One file? Opens directly. Five files? All loaded, ready to navigate.

find by content across entire tree, open in nvim
nvim $(find docs/modules/ROOT -name '*.adoc' -type f \
  -exec grep -l 'token.*expire\|oauth.*refresh' {} \;)
open one at a time (sequential — -exec nvim per match)
find -path '*oauth*' -name '*.adoc' -type f \
  -exec grep -l 'timeout\|expire' {} \; \
  -exec nvim {} \;
Trailing \| in grep patterns matches empty string — every file matches. Always end with a term, not a pipe: 'timeout\|expire\|token' not 'timeout\|expire\|token\|'.

Trace Antora partial inclusion chains

who includes this partial? (one level up)
grep -rl 'commands/shell' docs/modules/ROOT/partials/
count all pages that include a partial
grep -rl 'quick-commands' docs/modules/ROOT | wc -l
full chain: partial → assembler → every page that uses it
file="commands/shell"
grep -rl "$file" docs/modules/ROOT/partials/ | while read f; do
  parent=$(basename "$f" .adoc)
  echo "$file -> $parent"
  grep -rl "$parent" docs/modules/ROOT/pages/ | while read p; do
    echo "  -> $(basename "$p")"
  done
done

Pattern: grep -rl finds which files contain the string. Chain two passes — first finds the assembler partial, second finds every page that includes it. Works for any partial in the Antora include hierarchy.

Multi-pattern file search — worklog partial discovery

brute force — one find per partial name
find docs/modules/ROOT -name "*urgent.adoc*" -type f
find docs/modules/ROOT -name "*morning.adoc*" -type f
consolidated — single find with regex (production approach)
find docs/modules/ROOT -type f -regextype posix-extended \
  -regex '.*(urgent|morning|work-chla|personal|education|infrastructure|quick-commands|related)\.adoc' \
  | sort

Pattern: -regextype posix-extended enables | alternation without escaping. One process, one sort — versus 8 separate finds. The sort deduplicates visually and groups by path.

pipeline alternative — find piped to grep
find docs/modules/ROOT -type f -name "*.adoc" \
  | grep -E 'urgent|morning|work-chla|personal|education|infrastructure|quick-commands|related'

Trade-off: the pipeline version is more readable but spawns two processes. The regex version is a single find — faster on large trees, same result.

Cross-repo literary term search — bibliotheca-wide discovery

When searching for a term across the entire _bibliotheca (multiple repos, mixed file types), these patterns escalate from narrow to broad.

1. Single repo — count matches per file
grep -rn --include='*.adoc' -c 'sanchuelo' . | grep -v ':0$'
2. Cross-repo — filenames only (all bibliotheca)
grep -rl --include='*.adoc' -i 'sanchuelo' ~/atelier/_bibliotheca/ | sort
3. Cross-repo with context — see the line in situ
grep -rn --include='*.adoc' -i -B1 -A1 'sanchuelo' ~/atelier/_bibliotheca/domus-captures/
4. Multi-filetype — .adoc + .txt (catches source texts)
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.txt' --include='*.adoc' | sort
5. Null-safe find + xargs — handles spaces in paths
find ~/atelier/_bibliotheca/ -type f \( -name '*.adoc' -o -name '*.txt' \) -print0 \
  | xargs -0 grep -li 'sanchuelo' | sort
6. Open all hits directly in nvim
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.adoc' --include='*.txt' | xargs nvim

Pattern escalation: #1 confirms the term exists and where. #2 expands to all repos. #3 shows context without opening files. #4 adds plain text sources (Quijote .txt originals). #5 is the safe version for automation. #6 opens everything for editing.

Trade-off: grep -r --include is faster for known file types. find | xargs grep is safer for paths with spaces and more extensible (add -name '*.md' etc.). For literary searches across the bibliotheca, #4 or #5 is usually the right starting point — the source texts are .txt, not .adoc.

Search daily notes — find commands, builds, and patterns across worklogs

Source: 2026-06-22 — searching for build-antora-page invocations and theme usage across partials

which daily note partials mention a command?
# Find files — filenames only
grep -rl 'build-antora-page' docs/modules/ROOT/partials/worklog/
see the match in context without opening the file
# 3 lines of context around each hit
grep -rn -C3 'build-antora-page' docs/modules/ROOT/partials/worklog/
search for any theme flag across all partials (PCRE alternation)
grep -rPn 'theme\s+(light-cyan|catppuccin|mocha)' docs/modules/ROOT/partials/
find + grep — search all daily note partials for build/export patterns
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec grep -l 'build-antora-page\|build-adoc\|--theme\|pdf' {} \;
find + awk — show filename:matching_line for every hit (no -r needed)
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec awk '/build-antora-page|build-adoc|--theme/{print FILENAME": "$0}' {} \;
awk state machine — extract full [source,bash] blocks containing a command
find docs/modules/ROOT/partials -name '*.adoc' -exec \
  awk '/^\[source,bash\]/{block=1; buf=""} \
       block{buf=buf"\n"$0} \
       /^----$/ && block>1{if(buf~/build-antora-page/) print FILENAME":"buf; block=0} \
       block{block++}' {} \;

The awk state machine: [source,bash] sets block=1 and starts buffering. Each line appends to buf. When the closing ---- arrives (and block>1 to skip the opening fence), check if the buffer contains the target command. If yes, print filename + entire block. Reset. One pass, arbitrary block size, no temp files.

sed address range — print only lines between code fences matching a pattern
find docs/modules/ROOT/partials/worklog -name '*.adoc' -exec \
  sed -n '/\[source,bash\]/,/^----$/{/build-antora-page/p}' {} +
search data/ too — standalone adoc outside Antora
grep -rn 'build-antora-page\|build-adoc' data/ --include='*.adoc'
every invocation sorted by path (chronological by date directory)
grep -rn 'build-antora-page' docs/modules/ROOT/partials/worklog/daily-notes/ \
  | awk -F: '{print $1, $3}' | sort

Pattern escalation: grep -rl → "where is it". grep -rn -C3 → "what’s around it". find -exec awk → "extract the structured block". sed address ranges → "print between delimiters". Each tool has a different affordance — grep finds, awk extracts structure, sed filters ranges, find -exec scales across trees.

Email thread analysis — extract people, dates, commitments, silence

who’s in the thread (@ mentions + From headers)
grep -P '(@\w+|^From:.*<)' comms.adoc
timeline — every date with context
grep -nP '\d{1,2}/\d{1,2}/\d{2,4}|20\d{2}-\d{2}-\d{2}' comms.adoc
commitments — who promised what
grep -niP '(I can |I will |I.ll |we will |we.ll )' comms.adoc
open questions and unknowns
grep -niP '(\?|need to confirm|need to validate|TBD|pending)' comms.adoc

comm — set difference (who hasn’t replied)

# All recipients
grep -oP '<\K[^>]+' comms.adoc | sort -u > /tmp/all-recipients

# All senders
grep -P '^From:' comms.adoc | grep -oP '<\K[^>]+' | sort -u > /tmp/replied

# Who's silent — follow-up targets
comm -23 /tmp/all-recipients /tmp/replied

comm -23 outputs lines only in file 1 (recipients not in senders). Requires sorted input. grep -oP '<\K[^>]+' uses PCRE lookbehind — match < but don’t include it, capture until >.

Sort find results by modification time (newest first)

find discovers files but has no sort. Chain -printf with sort to order by mtime.

awk '{print $2}' truncates filenames with spaces — Familia Romana_ Lingva…​ becomes Familia. Always use the null-safe or sub() variants below for real data.
epoch sort — space-safe (production version)
# Sort by mtime, strip epoch prefix — handles spaces in filenames
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T@ %p\n' | sort -rn | awk '{sub(/^[^ ]+ /,""); print}'

sub(/[ ]+ /,"") removes everything up to and including the first space (the epoch). {print $2} would split on every space — fatal for Familia Romana_ Lingva Latina.

human-readable timestamps alongside
# ISO 8601 timestamps — readable and lexicographically sortable
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T+ %p\n' | sort -r | head -20

%T+ renders YYYY-MM-DD+HH:MM:SS — no epoch math needed, still sorts correctly as text.

null-safe — the bulletproof version
# Null-delimited: survives any filename (newlines, quotes, unicode)
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T@\t%p\0' | sort -zrn | awk -v RS='\0' -F'\t' '{print $2}'

-printf '%T@\t%p\0' — tab separates epoch from path, null terminates. sort -z sorts null-delimited records. awk -v RS='\0' -F'\t' reads null-terminated, splits on tab — $2 is now the full path regardless of spaces.

stat fallback — portable (BSD/macOS)
# GNU stat equivalent — works where -printf is unavailable
find ~/Downloads -maxdepth 1 -name '*latin*' -exec stat --format='%Y %n' {} + | sort -rn | awk '{sub(/^[^ ]+ /,""); print}'

-exec …​ {} + batches all files into one stat call (faster than \;). On macOS, use stat -f '%m %N' instead of --format='%Y %n'.

File intelligence — size, type, duplicates, age

Beyond finding files — interrogating them.

top 10 largest files in a directory tree
# Size in bytes (-printf %s), human-readable via numfmt
find ~/Downloads -type f -printf '%s\t%p\n' | sort -rn | head -10 | numfmt --to=iec --field=1

numfmt --to=iec --field=1 converts the first field from bytes to K/M/G. sort -rn on raw bytes is exact — ls -lhS rounds and sometimes mis-sorts.

find duplicates by size (fast pre-filter before checksumming)
# Files sharing a byte count — likely duplicates (confirm with md5sum)
find ~/Downloads -type f -printf '%s %p\n' | awk '{seen[$1]++; files[$1]=files[$1] "\n  " $0} END {for (s in seen) if (seen[s]>1) print files[s]}'
find duplicates by content — definitive
# md5sum only files with duplicate sizes (two-pass: fast then precise)
find ~/Downloads -type f -printf '%s\n' | sort | uniq -d | while read -r size; do
  find ~/Downloads -type f -size "${size}c" -exec md5sum {} +
done | sort | uniq -w32 -D

Two-pass: first find duplicate sizes (cheap), then md5sum only those (expensive). uniq -w32 -D compares first 32 chars (the hash) and prints all duplicates.

file type census — what’s actually in this directory?
# Count files by MIME type (not extension — extensions lie)
find ~/Downloads -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn

file --mime-type -b reports actual content type. -b suppresses filename. A .pdf that’s really text/html is a failed download.

stale files — untouched for 30+ days
# Files not accessed in 30 days — candidates for cleanup
find ~/Downloads -maxdepth 1 -type f -atime +30 -printf '%A+ %s\t%p\n' | sort | numfmt --to=iec --field=2

-atime 30` = access time older than 30 days. `-printf '%A' shows last access. Useful for Downloads cleanup without deleting something you just renamed.

disk usage by subdirectory — sorted
# Which subdirectories consume the most space?
find . -maxdepth 1 -type d -exec du -sh {} + 2>/dev/null | sort -rh | head -20

Batch operations — rename, move, transform

rename all files — strip spaces, lowercase, normalize unicode
# Dry run — show what would change (remove echo to execute)
find ~/Downloads -maxdepth 1 -type f -name '* *' -print0 | while IFS= read -r -d '' f; do
  dir=$(dirname "$f")
  base=$(basename "$f" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
  echo mv "$f" "$dir/$base"
done

IFS= read -r -d '' — the holy trinity for null-safe filename reading. IFS= prevents whitespace trimming. -r prevents backslash interpretation. -d '' reads until null.

move files by extension into categorized subdirectories
# Sort Downloads chaos into folders by type
find ~/Downloads -maxdepth 1 -type f -print0 | while IFS= read -r -d '' f; do
  ext="${f##*.}"
  case "$ext" in
    pdf|epub)    dest="books" ;;
    jpg|png|svg) dest="images" ;;
    sh|py|rb)    dest="scripts" ;;
    *)           dest="other" ;;
  esac
  mkdir -p ~/Downloads/"$dest"
  echo mv "$f" ~/Downloads/"$dest"/
done

${f##.} — parameter expansion: strip longest match of . from front, leaving only the extension. No basename or awk needed.

batch convert epub → asciidoc (like your Cicero fetch script)
# Convert all epubs in a directory to asciidoc via pandoc
find . -name '*.epub' -type f -exec sh -c '
  for epub; do
    adoc="${epub%.epub}.adoc"
    pandoc -f epub -t asciidoc "$epub" -o "$adoc" \
      && printf "  → %s (%s lines)\n" "$adoc" "$(wc -l < "$adoc")" \
      || printf "  ✗ failed: %s\n" "$epub"
  done
' _ {} +

-exec sh -c '…​' _ {} + — batch mode. _ fills $0 (script name, discarded). All matched files become $1, $2, …​ iterated by for epub. One sh invocation, not one per file.

xargs power patterns

parallel processing — 4 cores
# Checksum all PDFs in parallel (4 processes)
find ~/Downloads -name '*.pdf' -print0 | xargs -0 -P4 md5sum

-P4 runs 4 md5sum processes simultaneously. -print0 | xargs -0 is the null-safe pipeline — no filename can break it.

batched execution — two arguments at a time
# Compare files pairwise with diff
find . -name '*.adoc' -print0 | xargs -0 -n2 diff --brief

-n2 feeds two arguments per invocation. Useful for pairwise comparisons, copy operations (-n2 with cp), or any command taking exactly two args.

placeholder — insert filename at specific position
# Backup every config file: cp <file> <file>.bak
find /etc -maxdepth 1 -name '*.conf' -print0 | xargs -0 -I{} cp {} {}.bak

-I{} replaces {} with each filename. Slower than + batching (one cp per file) but necessary when the filename must appear in a specific position.

Process substitution — diff without temp files

compare two directory listings
# What files exist in study-A but not study-B?
diff <(find data/d000/education/ciceron-study -type f -name '*.adoc' | sort) \
     <(find data/d000/education/latin-study -type f -name '*.adoc' | sort)

<(cmd) creates a file descriptor from command output. diff sees two "files" — no temp files created, no cleanup needed.

compare file counts across directories
# Side-by-side: file type census of two directories
paste <(find dir1 -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn) \
      <(find dir2 -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn)

awk, sed, jq

awk — field extraction

print second field (whitespace-delimited)
awk '{print $2}' file.txt
custom delimiter — colon-separated (like /etc/passwd)
awk -F: '{print $1, $3}' /etc/passwd
extract JSON code blocks from AsciiDoc
awk '/\[source,json\]/{getline; if ($0 ~ /^----/) {p=1; next}} p && /^----/{p=0; next} p' file.adoc
field extraction with printf formatting
awk '{printf "%-30s %s\n", $1, $2}' file.txt

sed — stream editing

in-place replacement with verify-before/after
# Before
awk 'NR==73' /etc/ssh/sshd_config
# Change
sed -i '73s/#GSSAPIAuthentication no/GSSAPIAuthentication yes/' /etc/ssh/sshd_config
# After
awk 'NR==73' /etc/ssh/sshd_config
extract line range
sed -n '10,20p' file.txt

sed — line-targeted replacement (verify-before / change / verify-after)

the full pattern: locate → validate → change → verify
# 1. LOCATE: find the line number
grep -n 'adoc-pdf' zsh/.zshrc

# 2. VALIDATE: read the exact line before changing
awk 'NR==1760' zsh/.zshrc

# 3. CHANGE: target by line number — only hits that line
sed -i '1760s/alias adoc-pdf=/alias build-adoc=/' zsh/.zshrc

# 4. VERIFY: confirm change AND check for collateral
grep -n 'build-adoc\|adoc-pdf' zsh/.zshrc

Without the line number prefix (1760s/), sed replaces every match in the file — a shotgun. With it, surgical. The line number comes from grep -n.

multi-line verify — check two specific lines at once
awk 'NR==1218 || NR==1760' zsh/.zshrc
range extraction — NR for surgical reads from large files
# grep found the error at line 44164 — read 50 lines of context
awk 'NR>=44160 && NR<=44210' session-dump.adoc

No head | tail chains. No sed -n '44160,44210p'. One awk, two numbers.

grep -oP with \K — value extraction from key-value logs

extract just the value after a key (Perl regex)
# ISE syslog — extract failure reasons
grep -oP 'FailureReason=\K[^,;]+' /var/log/syslog | sort | uniq -c | sort -rn

# ISE — extract MAC addresses
grep -oP 'Calling-Station-ID=\K[0-9A-Fa-f:.-]+' /var/log/syslog | sort -u

# ISE — extract NAS IPs
grep -oP 'NAS-IP-Address=\K[0-9.]+' /var/log/syslog | sort -u

# ISE — extract device names
grep -oP 'NetworkDeviceName=\K[^,;]+' /var/log/syslog | sort -u

\K resets the match start — everything before \K is required context but excluded from output. [^,;]+ captures until the next delimiter. Pipe to sort -u for unique, sort | uniq -c | sort -rn for counted frequency.

pattern: grep -oP 'KEY=\KVALUE_REGEX' | sort pipeline
# Generic form — works for any key=value log format
grep -oP 'FIELD_NAME=\K[^,;]+' logfile | sort | uniq -c | sort -rn | head -20

jq — JSON processing

extract nested fields
curl -s localhost:8080/stats | jq '.stats.total_files'
filter array by property
jq '.results[] | select(.category == "standards")' response.json
transform to TSV for spreadsheets
jq -r '.[] | [.title, .path] | @tsv' response.json | column -t -s $'\t'
GitHub API + jq — commit history by path
gh api "repos/EvanusModestus/domus-captures/commits?path=docs/&per_page=10" |
  jq -r '.[] | "\(.commit.author.date[:10]) \(.sha[:7]) \(.commit.message | split("\n")[0])"'

Shell Patterns

xargs — when the next command reads arguments, not stdin

Next command reads…​ Use

stdin (awk, grep, wc, sort)

pipe directly

arguments (stat, rm, cp, nvim, git add)

xargs

copy today’s files to backup — -I{} placeholder
mkdir -p /tmp/adoc-backup-$(date +%F) && \
  find . -name "*.adoc" -type f -newermt "$(date +%F)" | \
  xargs -I{} cp {} /tmp/adoc-backup-$(date +%F)/
parallel validation — -P4 runs 4 at a time
find .drafts -name "*.adoc" -type f | xargs -P4 -I{} asciidoctor -o /dev/null {}
null-delimited pipeline — safe for filenames with spaces
find . -name "*.adoc" -type f -print0 | xargs -0 wc -l

Process substitution — <(cmd) treats output as a file

compare tracker state: yesterday vs today
diff <(grep '|' partials/trackers/work/adhoc/carryover.adoc | head -20) \
     <(git show HEAD~1:partials/trackers/work/adhoc/carryover.adoc | grep '|' | head -20)
files on disk vs files in nav — drift detection
diff <(find docs/modules/ROOT/pages/projects/chla/mschapv2-migration -name "*.adoc" -type f | sort) \
     <(grep -oP 'mschapv2-migration/[^[]+\.adoc' docs/modules/ROOT/nav.adoc | sort)

Command substitution — embed output as arguments

open most recently modified file in nvim
nvim "$(find data/ -name '*.adoc' -type f -printf '%T@ %p\n' | sort -rn | awk 'NR==1{print $2}')"
line count across a project
wc -l $(find docs/modules/ROOT -path '*mschapv2*' -name '*.adoc' -type f)

Conditional execution — capture, test, act

open matching files only if they exist
files=$(find .drafts -name 'in*' -type f) && [ -n "$files" ] && nvim $files
open files that contain unchecked items
files=$(grep -rl '\[ \]' .drafts/*.adoc) && [ -n "$files" ] && nvim $files
guard with grep -q — only act if pattern matches
grep -q 'TODO\|FIXME\|\[ \]' "$file" && nvim "$file"

Pattern: $(capture)[ -n ] tests non-empty → && only proceeds if true. grep -q is the idempotent guard — run repeatedly, only opens when there’s work.

Decrypt and open — find .age, decrypt, nvim in one shot

files=$(find . -name "*tcp-clock*.age" -type f) && \
  [ -n "$files" ] && echo "$files" | xargs -I{} decrypt-file {} && \
  nvim $(echo "$files" | sed 's/\.age$//')

Pattern: find .age only (never tries plaintext), sed derives the decrypted path, guard prevents empty nvim. Change the glob to match any project.

tee_clean — color on screen, clean text in file

tee_clean() {
  tee >(sed 's/\x1b\[[0-9;]*m//g' > "$1")
}

# Color output on terminal, stripped in file
jq -C '.' data.json | tee_clean output.json
xq -C '.' data.xml | tee_clean output.json

# Wrap a whole block
{
  echo "=== Summary ==="
  jq -C '.[] | .name' data.json
} | tee_clean summary.txt

The >(cmd) is process substitution — tee writes to stdout AND to the subshell pipe. sed strips ANSI escape sequences (\x1b\[[0-9;]*m) before they hit the file.

Dependency check — verify toolchain in one shot

for cmd in asciidoctor asciidoctor-pdf pandoc rouge d2 mmdc age; do
  printf "%-20s %s\n" "$cmd" "$(command -v $cmd >/dev/null 2>&1 && echo 'OK' || echo 'MISSING')"
done

Pattern: command -v checks if binary exists on PATH. >/dev/null 2>&1 suppresses output — we only care about exit code. Swap the tool list for any project’s dependencies.

printf safety — dashes as data, not options

wrong — printf treats --- as invalid option
printf '---\n\n'
right — %s format string treats --- as data
printf '%s\n\n' '---'

Kill stuck SSH sessions

Find established SSH connections
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2, $9}'
Kill all stuck SSH sessions to a specific host
lsof -i TCP -n -P | awk '/ssh.*kvm-01.*ESTABLISHED/ {print $2}' | sort -u | xargs kill
Kill ALL stuck SSH sessions
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2}' | sort -u | xargs kill

lsof -i TCP -n -P lists all TCP connections. awk filters for SSH + ESTABLISHED, prints only the PID ($2). sort -u deduplicates (multiple file descriptors per process). xargs kill sends SIGTERM to each.

File Descriptors & Redirection

The three file descriptors

FD Name Purpose

0

stdin

input to the command

1

stdout

normal output (valid results)

2

stderr

error messages

Split stdout and stderr into separate files

find / -name "*.conf" 1>results.txt 2>errors.txt

Suppress errors — 2>/dev/null

find / -name "*.conf" 2>/dev/null

Merge stderr into stdout — 2>&1

command 2>&1 | grep "pattern"

This sends both stdout and stderr through the pipe. Without 2>&1, only stdout reaches grep — errors print to the terminal and bypass the pipeline.

Heredoc patterns

multi-line input to a command
cat <<'EOF'
Line 1
Line 2
EOF
heredoc commit messages (quotes prevent variable expansion)
git commit -m "$(cat <<'EOF'
feat: add new feature

Multi-line description here.
EOF
)"

API & curl/jq

domus-api — Documentation System REST API

start the API server
cd ~/atelier/_projects/personal/domus-api && uv run uvicorn domus_api.main:app --host 0.0.0.0 --port 8080
health check
curl -s localhost:8080/ | jq
full-text search
curl -s 'localhost:8080/search?q=mandiant' | jq
search — extract path, title, match count
curl -s 'localhost:8080/search?q=mandiant' | jq '.results[] | {path, title, match_count}'
list pages by category
curl -s 'localhost:8080/pages?category=standards' | jq
all antora.yml attributes
curl -s localhost:8080/attributes | jq

GitHub API

cross-repo search via GitHub API
gh search code "vault seal" --owner EvanusModestus --json repository,path,textMatches |
  jq '.[] | {repo: .repository.full_name, file: .path, match: .textMatches[].fragment}'
count .adoc files in a repo via API
gh api 'repos/EvanusModestus/domus-captures/git/trees/main?recursive=1' |
  jq '[.tree[] | select(.path | endswith(".adoc"))] | length'

Domus Workflows

Read content from terminal (meeting-ready)

today’s worklog
bat docs/modules/ROOT/pages/2026/04/WRKLOG-$(date +%Y-%m-%d).adoc
current priorities
bat docs/modules/ROOT/partials/trackers/work/priorities/current.adoc
carryover backlog
bat docs/modules/ROOT/partials/trackers/work/adhoc/carryover.adoc
any project summary
bat docs/modules/ROOT/partials/projects/mandiant-remediation/summary.adoc

Search and discovery

find all files related to a topic
grep -rl "MSCHAPv2" docs/modules/ROOT/ --include="*.adoc" | sort
search codex entries
grep -rn "pattern" docs/modules/ROOT/partials/codex/ --include="*.adoc" -B1 -A3
list all worklogs for a month
ls -1 docs/modules/ROOT/pages/2026/04/WRKLOG-*.adoc

Tracker aging — calculate days from origin

how many days since a carryover item started
echo $(( ($(date +%s) - $(date -d "2026-03-09" +%s)) / 86400 ))

Encrypted data access (d001)

view encrypted file without disk write
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
  data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
  | bat --language asciidoc
project encryption dashboard
for d in data/d001/projects/*/; do
  total=$(find "$d" -type f | wc -l)
  plain=$(find "$d" -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | wc -l)
  printf "%-25s %s files  %s plaintext\n" "$(basename "$d")" "$total" "$plain"
done

d000 study builds

batch build all docs for a Quijote chapter range
for d in p1-cap-03{7,8,9}; do
  for f in data/d000/education/quijote-study/notas/$d/*.adoc; do
    d000 build "$d/$(basename "$f" .adoc)" html --variant light-cyan
  done
done
build a single study doc (use unique path fragment)
d000 build p1-cap-038/texto-anotado html --variant light-cyan
d000 build p1-cap-038/texto-anotado pdf --theme light-cyan
batch build PDFs for a chapter range
for d in p1-cap-03{7,8,9}; do
  for f in data/d000/education/quijote-study/notas/$d/*.adoc; do
    d000 build "$d/$(basename "$f" .adoc)" pdf --theme light-cyan
  done
done
open all rendered chapters at once
firefox data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.html &
firefox data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf &
print chapter PDFs (requires CUPS configured)
lp data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf
build LPL logic study (English / Spanish)
d000 build annotated-text pdf --theme light-cyan
d000 build lpl-study/notas/texto-anotado pdf --theme light-cyan
build Cicero study
d000 build de-oratore/libro-i/texto-anotado html --variant light-cyan

Available themes

PDF themes (--theme)
ls ~/atelier/_bibliotheca/domus-asciidoc-build/themes/pdf/ | sed 's/-theme\.yml//'
# base blue burgundy catppuccin creative dark don-quijote green
# learning light-cyan navy operations orange purple reference royal
HTML variants (--variant)
~/atelier/_bibliotheca/domus-asciidoc-build/docinfo/compose.sh --list
# light dark catppuccin royal light-cyan

ISE & Network Ops

ISE ERS API — endpoint CRUD

set credentials (session)
export ISE_HOST="{ise-ip}" ISE_USER="admin" ISE_PASS="$(gopass show -o ise/admin)"
list identity groups
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/identitygroup" \
  -H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.resources[].name'
check if endpoint exists by MAC
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/endpoint?filter=mac.EQ.AA:BB:CC:DD:EE:FF" \
  -H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.total'

Certificate inspection

view EAP-TLS client cert from local store
openssl x509 -in {cert-dir}/client.pem -text -noout | head -30
check cert expiry
openssl x509 -in {cert-dir}/client.pem -enddate -noout

Network diagnostics

check listening ports
ss -tlnp | grep -E ':{port-https}|:{port-ssh}|:{port-ldaps}'
test ISE connectivity
nc -zv {ise-ip} {ise-ers-port}
DNS resolution
dig {ise-hostname} +short

ISE eval rotation — backup & restore

backup from ISE CLI (when admin UI is license-locked)
# SSH to ISE
ssh admin@ise-02.inside.domusdigitalis.dev

# Verify NAS repo
show repository nas-01

# Get encryption key (on workstation)
dsource d000 dev/storage
echo $ISE_BACKUP_KEY

# Run backup
backup pre-rotation-2026-06 repository nas-01 ise-config encryption-key plain <KEY>
list backups on NAS
ssh admin@ise-02.inside.domusdigitalis.dev
show repository nas-01
restore to fresh ISE node
configure terminal
repository nas-01
  url nfs://10.50.1.70:/volume1/ise_backups
exit

restore <backup-filename> repository nas-01 encryption-key plain <KEY>

VyOS — VRRP & VLAN inspection

VRRP status and VIPs
show vrrp
show configuration commands | grep vrrp | grep 'address'
firewall zone membership
show configuration commands | grep 'firewall zone' | grep 'member'
DHCP leases and ARP
show dhcp server leases
show arp
full interface/VLAN map
show interfaces

CUPS printing — validation & setup

software validation
command -v lpstat && echo "CUPS present" || echo "CUPS not installed"
lpstat -r                                # scheduler running?
lpstat -p -d                             # printers + default
daemon lifecycle
sudo systemctl enable --now cups         # start + persist
printer discovery
lpinfo -v                                # available backends/URIs
lpinfo -m | grep -i <brand>             # available drivers
add printer and set default
sudo lpadmin -p <name> -v <uri> -m everywhere -E
lpoptions -d <name>
print
lp file.pdf                              # default printer
lp -d <name> -o sides=two-sided-long-edge file.pdf

PowerShell (from zsh)

All PowerShell commands run inside pwsh -NoLogo -Command '…​' from zsh. Running them bare fails — zsh interprets $, |, () as shell syntax.

Process management

top 5 processes by memory
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 5 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | Format-Table'
stop/start Teams
pwsh -NoLogo -Command 'Get-Process | Where-Object {$_.ProcessName -like "*teams*"} | Stop-Process'
pwsh -NoLogo -Command 'Start-Process "ms-teams"'

Export to JSON (pipe to jq)

always use -NoLogo when piping pwsh output to zsh tools
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 5 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | ConvertTo-Json' | jq '.'
Never pipe Format-Table into ConvertTo-Json — it produces layout metadata, not data. Select-Object first, then ConvertTo-Json.

Wi-Fi management (netsh)

force fresh network scan
netsh wlan disconnect interface="Wi-Fi"
netsh wlan show networks mode=bssid
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"

SSH from PowerShell

connect to homelab from Windows terminal
ssh evan@modestus-razer.inside.domusdigitalis.dev

WSL ↔ Windows — Cross-Environment Commands

From zsh (WSL) — control Windows
run any PowerShell command from zsh
pwsh -NoLogo -Command 'Get-Date'
run multi-line PowerShell from zsh (heredoc)
pwsh -NoLogo -Command "$(cat <<'PS'
$procs = Get-Process | Where-Object { $_.WorkingSet64 -gt 100MB }
$procs | Sort-Object WorkingSet64 -Descending |
  Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize
PS
)"
open a file in Windows from WSL
# Open in default Windows app
wslview /mnt/c/Users/erosado/Documents/report.pdf

# Open Explorer to current WSL directory
explorer.exe .

# Open specific Windows path
explorer.exe 'C:\Users\erosado\Downloads'
copy WSL output to Windows clipboard
# Pipe anything to Windows clipboard
cat file.txt | clip.exe

# Copy a command's output
pwsh -NoLogo -Command 'Get-TransportRule | Format-List Name, State' | clip.exe
access Windows files from WSL
# Windows C: drive is at /mnt/c
ls /mnt/c/Users/erosado/Downloads/

# Copy from Windows to WSL
cp /mnt/c/Users/erosado/Downloads/report.pdf ~/atelier/

# Watch a Windows directory for new files
find /mnt/c/Users/erosado/Downloads -maxdepth 1 -mmin -5 -type f -printf '%T+ %p\n' | sort -r
From PowerShell — control WSL
run a bash command from PowerShell
wsl -e bash -c 'grep -rn "Ghost-Sender" ~/atelier/_bibliotheca/domus-captures/docs/'
run a specific WSL command and capture output
$result = wsl -e bash -c 'git -C ~/atelier/_bibliotheca/domus-captures log --oneline -5'
$result
Process Management — Windows Side
top processes by memory — formatted table
pwsh -NoLogo -Command '
Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 20 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}},
    @{N="CPU(s)";E={[math]::Round($_.CPU,1)}},
    @{N="Handles";E={$_.HandleCount}} |
  Format-Table -AutoSize'
find a specific process
pwsh -NoLogo -Command 'Get-Process | Where-Object { $_.ProcessName -like "*teams*" } |
  Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize'
kill by name
pwsh -NoLogo -Command 'Stop-Process -Name "Teams" -Force -ErrorAction SilentlyContinue'
kill by PID
pwsh -NoLogo -Command 'Stop-Process -Id 12345 -Force'
what’s listening on a port (Windows equivalent of ss -tulnp)
pwsh -NoLogo -Command 'Get-NetTCPConnection -State Listen |
  Select-Object LocalAddress, LocalPort, OwningProcess,
    @{N="Process";E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
  Sort-Object LocalPort | Format-Table -AutoSize'
specific port check
pwsh -NoLogo -Command 'Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue |
  Select-Object LocalAddress, LocalPort, RemoteAddress, State,
    @{N="Process";E={(Get-Process -Id $_.OwningProcess).ProcessName}}'
Services — Windows Side
list running services
pwsh -NoLogo -Command 'Get-Service | Where-Object { $_.Status -eq "Running" } |
  Sort-Object DisplayName | Format-Table Name, DisplayName, Status -AutoSize'
check a specific service
pwsh -NoLogo -Command 'Get-Service -Name "WinRM" | Format-List Name, DisplayName, Status, StartType'
restart a service
Restart-Service -Name "WinRM" -Force
System Info — Quick Health from zsh
one-shot Windows system summary
pwsh -NoLogo -Command '
Write-Host "=== Windows System ===" -ForegroundColor Cyan
Write-Host "Hostname: $env:COMPUTERNAME"
Write-Host "User:     $env:USERNAME"
Write-Host "OS:       $((Get-CimInstance Win32_OperatingSystem).Caption)"
Write-Host "Uptime:   $((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime)"
Write-Host "RAM:      $([math]::Round((Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize/1MB))GB total, $([math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory/1MB))GB free"
Write-Host "CPU:      $((Get-CimInstance Win32_Processor).Name)"
Write-Host "Disk C:   $([math]::Round((Get-PSDrive C).Free/1GB))GB free of $([math]::Round(((Get-PSDrive C).Used + (Get-PSDrive C).Free)/1GB))GB"'
disk usage — all drives
pwsh -NoLogo -Command 'Get-PSDrive -PSProvider FileSystem |
  Select-Object Name, @{N="Used(GB)";E={[math]::Round($_.Used/1GB,1)}},
    @{N="Free(GB)";E={[math]::Round($_.Free/1GB,1)}},
    @{N="Total(GB)";E={[math]::Round(($_.Used+$_.Free)/1GB,1)}} |
  Format-Table -AutoSize'
Exchange Online — Connect from zsh
connect to Exchange Online (launches MFA prompt in Windows)
pwsh -NoLogo -Command 'Connect-ExchangeOnline -UserPrincipalName erosado@chla.usc.edu'
MFA prompt opens in the Windows browser. After auth, the session persists in the pwsh process. For multi-command sessions, start pwsh interactively instead of one-shot commands.
interactive PowerShell session from zsh (for Exchange, etc.)
pwsh -NoLogo
# Then inside pwsh:
# Connect-ExchangeOnline
# Get-TransportRule | Format-List Name, State
# exit
File Transfer Patterns
move files between WSL and Windows
# WSL → Windows Downloads
cp ~/atelier/_bibliotheca/domus-captures/output/report.pdf /mnt/c/Users/erosado/Downloads/

# Windows → WSL (glob)
cp /mnt/c/Users/erosado/Downloads/*.{png,pdf,jpg} ~/atelier/_staging/

# Bulk move with null safety
find /mnt/c/Users/erosado/Downloads -maxdepth 1 -name '*.pdf' -mmin -60 -print0 |
  xargs -0 -I{} cp {} ~/atelier/_staging/
watch Windows Downloads for new files (live)
inotifywait -m /mnt/c/Users/erosado/Downloads -e create -e moved_to |
  awk '{printf "%s  %s\n", strftime("%H:%M:%S"), $3}'
inotifywait requires inotify-tools. Install with sudo pacman -S inotify-tools if not present.

Security & Encryption

View encrypted files without writing to disk

pipe age decrypt to bat — nothing touches the filesystem
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
  data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
  | bat --language asciidoc --file-name "findings-status-2026-04-16.adoc"

Batch re-encrypt — brace expansion + loop

re-encrypt multiple project files
for f in data/d001/projects/mandiant-remediation/{findings-status,guest-acl-update,siem-report}-2026-04-16.adoc; do
  rm -f "${f}.age" && echo y | encrypt-file "$f"
done
Always rm -f the .age first. If you skip it, encrypt-file prompts about overwrite and may only delete the plaintext without re-encrypting.

Detect stale plaintext — files needing re-encryption

find plaintext newer than its .age counterpart
for f in data/d001/projects/*/*.adoc; do
  age="${f}.age"
  if [ -f "$f" ] && [ -f "$age" ]; then
    pt_mod=$(/usr/bin/stat -c'%Y' "$f")
    age_mod=$(/usr/bin/stat -c'%Y' "$age")
    [ "$pt_mod" -gt "$age_mod" ] && echo "STALE: $f"
  fi
done

Secure delete — shred for sensitive plaintext

shred -u data/d001/projects/mandiant-remediation/man-report.txt
On SSD/NVMe, shred is less effective (wear leveling), but better than rm which only removes the directory entry.

Pre-push audit — find all unencrypted project files

find data/d001/projects -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | sort

System & Infrastructure

PipeWire audio validation

wpctl status                                    # PipeWire status
pactl list sinks short                          # list audio sinks
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga  # test default sink
journalctl -b --grep='sof|cs35l56' --no-pager | tail -20  # kernel audio firmware
cat /proc/asound/cards                          # ALSA sound cards

gopass — personal document management

gopass-personal-docs    # interactive entry creation
gopass-query bills      # list recurring bills with totals
gopass-query storage    # list storage units with gate codes
gopass-query export bills  # export category to JSON

Makefile — daily workflow

make new-day      # create today's worklog + update attributes
make serve        # build + local server (port 8000)
make              # build only
make sync-nav     # sync worklog nav entries
make update-index # rebuild monthly index

KVM — VM & ISO management

list VMs on a KVM host
ssh kvm-01 "sudo virsh list --all"
ssh kvm-02 "sudo virsh list --all"
find ISE ISOs across KVM hosts (case-insensitive glob)
ssh kvm-01 "ls -lh /mnt/nas/isos/*[Ii][Ss][Ee]* /var/lib/libvirt/images/*[Ii][Ss][Ee]* /mnt/onboard-ssd/isos/*[Ii][Ss][Ee]* 2>/dev/null"
ssh kvm-02 "ls -lh /mnt/nas/isos/*[Ii][Ss][Ee]* /mnt/ssd/libvirt/images/*[Ii][Ss][Ee]* 2>/dev/null"
console into a VM
sudo virsh console <vm-name>             # Escape: Ctrl+]
check NAS mount on KVM host
ssh kvm-01 "mount | grep nas; ls /mnt/"

Per-project file dashboard

per-project summary — total files vs unencrypted plaintext
for d in data/d001/projects/*/; do
  total=$(find "$d" -type f | wc -l)
  plain=$(find "$d" -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | wc -l)
  echo "$(basename "$d") | ${total} files | ${plain} plaintext"
done

USB-C / Thunderbolt Charging Diagnostics

Full evidence capture to file (one block, timestamped)
{
  echo "=== Power Supply ==="
  cat /sys/class/power_supply/*/status
  echo ""
  cat /sys/class/power_supply/*/type
  echo ""
  echo "=== UPower ==="
  upower -d | grep -E 'state|percentage|energy-rate|voltage'
  echo ""
  echo "=== dmesg (typec/thunderbolt/PD) ==="
  sudo dmesg | grep -iE 'typec|thunderbolt|ucsi|PD|power.delivery|charging' | tail -20
  echo ""
  echo "=== Pacman log (kernel/typec) ==="
  grep -iE 'thunderbolt|typec|ucsi|^.*upgraded linux ' /var/log/pacman.log | tail -20
} | tee /tmp/INC-$(date +%F)-usbc-charging.txt

Pattern: { } groups commands into a single stdout stream. tee writes to file AND displays on screen. Reusable for any multi-command evidence capture — change the commands inside, keep the structure.

pacman — package inspection

Source: 2026-06-25 — curl error 77 investigation, checking for package upgrades

# Check package version and install date (awk field filter)
pacman -Qi curl | awk '/^Version|^Install Date/'