Compare commits

...

18 Commits

Author SHA1 Message Date
Mortdecai 200de3d0b4 docs: handoff + checklist — /Volumes is hidden in Finder; open-over-ssh shows nothing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:53:54 -04:00
Mortdecai 0377aa3a0b docs: session handoff — tank automount
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:24:55 -04:00
Mortdecai c1558d2630 feat(tank): autofs SMB mount of tank at /Volumes/tank
scripts/tank-automount.sh (run on the Mac, password on stdin) adds a /-
direct map and root-owned /etc/auto_smb pointing at //Administrator@
192.168.0.173/tank with soft. Mounts on first access, unmounts idle, so
the laptop can leave the LAN without hung Finder or login dialogs.

Findings baked in:
- macOS automount does not create direct-map trigger dirs -> mkdir -p
- automounted fs are always nobrowse (man auto_master): shows as a
  folder, not a Locations drive -> Finder Favorite (manual-checklist)
- no-tty sudo ticket is per parent pid: sudo inside $(...) fails, so the
  idempotency check is a pipeline (sudo cmp -s -)
- SMB over NFS: Mac seth is uid 501; SMB force user=root matches /mnt/Z

Also: checklist Gitea key -> id_ed25519_homelab; the smb://.../tank
'user seth' step was wrong (no such Samba user) and is now the automount.
Wired into run.sh. Verified: mount, root-owned write, two all-[skip] reruns.
Not yet verified across a reboot (FileVault).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:24:00 -04:00
Mortdecai c204d99dc6 docs: session handoff — ssh aliases + two-key split
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:32:28 -04:00
Mortdecai 2028508cb9 feat(ssh): full homelab alias set on the Mac + separate interactive key
config/ssh_homelab now carries the curated alias set from ~/bin/CLAUDE.md
(pve nodes, machines, key CTs, switch, tailscale hosts). Interactive auth
uses a NEW key, id_ed25519_homelab, because the existing id_ed25519 is
rrsync-jailed on pve173 and sshd honours the first matching authorized_keys
line per key — one key can't be both a jail and a shell there.

- setup.sh generates id_ed25519_homelab (idempotent)
- backup.sh pins the jailed key: -o IdentitiesOnly=yes -i $HOME/.ssh/id_ed25519
- scripts/authorize-mac-key.sh (run on steel141) appends the pubkey on every
  LAN host via claude's aliases; wired into run.sh
- bedroom alias: seth -> root (matches steel141; was undocumented)

Verified from the Mac: 12 aliases land as the right user@host; the backup key
is refused a shell by rrsync (checked with id — rrsync whitelists true);
backup.sh still runs. Offline at push time: bedroom thinkcentre seth-pi
pinail openclaw2 — re-run authorize-mac-key.sh when they're up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:31:25 -04:00
Mortdecai 43877eb78f docs(checklist): kitty needs the Local Network grant — else LAN ssh = No route to host
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:15:29 -04:00
Mortdecai dc29b7a087 docs: session handoff — kitty titlebar fix
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:04:37 -04:00
Mortdecai 3399829eb6 fix(kitty): keep macOS titlebar — it's the only drag handle; color it to match bg
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:00:13 -04:00
Mortdecai d86ede0036 docs: session handoff — mac base setup shipped & verified
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 19:43:11 -04:00
Mortdecai 617fc789b1 docs: reconcile CLAUDE.md/DECISIONS with what shipped (sudo approach, rrsync, set -e lessons)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 19:41:58 -04:00
Mortdecai d622cf6aef fix: chmod 600 the ssh Include file (ssh rejects group-writable config)
rsync -a preserved steel141's 664 perms; ssh on the Mac then refused
~/mac/config/ssh_homelab as an Include target, breaking all Mac-side ssh
including the backup transport. setup.sh now chmods it 600 after each rsync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 19:40:58 -04:00
Mortdecai d7205a911c fix: pass pubkey to tank-side.sh via env, not ssh command arg
ssh re-splits remote command args on spaces, so -- "$PUB" arrived as $1=ssh-ed25519
(the key body and comment became $2/$3). The loose validation passed and grep -qF
matched any ed25519 key -> false '[skip] key present'; the key was never added.
Pass PUB via env (safe inside remote single-quotes) and require 'ssh-ed25519 AAAA*'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 19:39:33 -04:00
Mortdecai af51e0cfa3 fix: make SparkFun removal and launchd bootstrap non-fatal under set -e
networksetup -removenetworkservice fails on the sole PPP-port service and
aborted the whole run at exit 4 (before pmset/backup agent). Both it and the
GUI-domain launchctl bootstrap (flaky over SSH) are now best-effort with a
warn; the LaunchAgent plist auto-loads at next login regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 19:36:56 -04:00
Mortdecai 39f61f48ce fix: unattended sudo via temporary NOPASSWD drop-in with triple cleanup
Piping a password into ssh -tt's pty raced and hung on the real run (worked in
a fast test, blocked for 14 min in practice). Replace with a temp
/etc/sudoers.d/mac-setup installed by _install_sudoers.sh (validated
before activation), removed on every exit path plus a 40-min on-Mac
self-destruct. setup.sh now fails fast if passwordless sudo is absent
instead of depending on a primed tty ticket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 19:32:24 -04:00
Mortdecai 507bec3f46 feat: run.sh entry point and Tahoe manual checklist
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:09:45 -04:00
Mortdecai c47a220993 fix: tank-side.sh dataset check as explicit if (SC2015)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:09:24 -04:00
Mortdecai 6643ac003c feat: nightly rsync backup to tank with rrsync-restricted key and sanoid history
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:09:16 -04:00
Mortdecai 0d733c77e7 feat: setup.sh — preflight, homebrew, bash5 shell, hostname, Linux-feel defaults, Dock/debloat, DAW power profile
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:09:15 -04:00
17 changed files with 945 additions and 10 deletions
@@ -0,0 +1,141 @@
# Handoff: [TASK_TITLE - replace this]
## Session Metadata
- Created: 2026-09-15 19:42:09
- Project: /home/claude/bin/mac
- Branch: master
- Session duration: [estimate how long you worked]
### Recent Commits (for context)
- 617fc78 docs: reconcile CLAUDE.md/DECISIONS with what shipped (sudo approach, rrsync, set -e lessons)
- d622cf6 fix: chmod 600 the ssh Include file (ssh rejects group-writable config)
- d7205a9 fix: pass pubkey to tank-side.sh via env, not ssh command arg
- af51e0c fix: make SparkFun removal and launchd bootstrap non-fatal under set -e
- 39f61f4 fix: unattended sudo via temporary NOPASSWD drop-in with triple cleanup
## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
> This is the first handoff for this task.
## Current State Summary
Bootstrapped `~/bin/mac` from scratch: scaffolded the project, SSH'd into Seth's new MacBook Pro (M1 Pro, 32GB, macOS 26.2 Tahoe), brainstormed + specced + planned a debloat/Linux-feel/Ableton-first setup, then implemented and RAN it end-to-end. `scripts/run.sh` applied cleanly (exit 0, idempotent on re-run), and the tank backup path is proven working with rrsync jailing + sanoid history. Remaining work is entirely Seth's GUI-only manual checklist and installing Ableton Live — no more code needed to ship the base setup.
## Codebase Understanding
### Architecture Overview
Managed over SSH from steel141: `scripts/run.sh` rsyncs the repo to `~/mac` on the Mac, installs a TEMPORARY NOPASSWD sudoers drop-in (so Homebrew's long, child-spawning install has sudo), runs `scripts/setup.sh` there, removes the drop-in, pulls `defaults` backups to `.backup/mac/`. `setup.sh` is idempotent: each section prints `[skip]` or `[set]`. Config files (bashrc, kitty.conf, ssh_homelab) live in `config/` and are *included* by marker lines in the real dotfiles, so editing the repo edits the machine. Backup = launchd nightly rsync of `~/Music/Ableton`+`~/Documents` to `pve173:/tank/backups/mac`, key rrsync-jailed, sanoid `tank_media` template for history.
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `scripts/run.sh` | steel141 entry point (rsync+sudo drop-in+setup+cleanup) | run as `scripts/run.sh [--no-tank]`; needs `$HOMELAB_PASSWORD` |
| `scripts/setup.sh` | on-Mac idempotent configurator | 2nd run = all `[skip]` |
| `scripts/_install_sudoers.sh` | installs temp NOPASSWD drop-in (validated) | password piped to stdin |
| `scripts/backup.sh` | nightly rsync to tank | `/opt/homebrew/bin/rsync`, rrsync remote |
| `scripts/tank-side.sh` | pve173: dataset+sanoid+rrsync key | PUB via env, not arg |
| `docs/manual-checklist.md` | GUI-only Tahoe steps for Seth | the remaining work |
| `docs/reference/inventory-2026-09-15.md` | pre-change baseline | "was X stock?" |
### Key Patterns Discovered
- `set -euo pipefail` everywhere; optional/cosmetic steps MUST be `|| warn` or they abort the run (learned the hard way — SparkFun + launchctl bootstrap).
- SSH runs on macOS are NON-login: `source ~/.bash_profile` before expecting GNU PATH.
- ssh Include files must be `chmod 600` (rsync -a brings over group-writable 664, which ssh rejects).
- Passing args through `ssh host 'cmd' -- "$X"` re-splits on spaces remotely; pass via env instead.
- `defaults`/`pmset` backed up to `~/.mac-setup-backup/` before writes; pulled to `.backup/mac/`.
## Work Completed
### Tasks Finished
- [x] Project scaffolded, repo `git.sethpc.xyz/Seth/mac`, `ssh mac` alias
- [x] Inventory captured (pre-change)
- [x] Design spec + implementation plan written
- [x] Brewfile, setup.sh, run.sh, backup.sh, tank-side.sh, checklist written
- [x] `run.sh` applied on the Mac (exit 0), verified idempotent
- [x] Linux feel verified: bash 5.3.20, GNU sed, `ip`, hostname `mac`, tools in /opt/homebrew
- [x] DAW power profile, Dock=2 apps, defaults all applied
- [x] Tank backup proven: rrsync restriction enforced, upload + `--delete` + sanoid snapshots working
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| [no modified files detected] | | |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Temp NOPASSWD sudoers over pty password injection | Seth chose it; pty inject raced & hung 14 min | see DECISIONS.md |
| rrsync-jailed backup key | laptop key must not be unrestricted root on tank | enforced & proven |
| bash5 / no Cmd-swap / Rectangle / rsync-not-TimeMachine | Linux feel + DAW safety | DECISIONS.md |
## Pending Work
### Immediate Next Steps
1. Hand Seth `docs/manual-checklist.md` — the GUI-only steps (iCloud Drive Desktop&Documents OFF, DAW Focus, app first-launch grants for Rectangle/kitty/Tailscale, Screen Time/Handoff off).
2. Install Ableton Live 12 from ableton.com, then re-run `scripts/run.sh --no-tank` so the Dock picks up Live (setup.sh only adds it if `/Applications/Ableton Live 12*.app` exists).
3. Once an audio interface is attached: set it in Live > Settings > Audio (48kHz/128 buffer), verify no sleep/notifications during a 15-min AC session with DAW Focus on. Also unresolved: Traktor S4 MK1 macOS support (NI dropped it years ago) — verify before assuming it works as a Live controller.
### Blockers/Open Questions
- [ ] None blocking. Open: S4 MK1 macOS controller support unverified; audio interface not yet chosen/attached.
- [ ] FileVault is ON — after any reboot, `ssh mac` is dead until someone types the password at the lid.
### Deferred Items
- SparkFun PPP service left (macOS refuses removal of sole service on a port; harmless).
- Tailscale not yet logged in (GUI). AeroSpace tiling deferred (Rectangle chosen).
- Suite/third-party sample packs: in-app + SMB copy from Milkie, on the checklist.
## Context for Resuming Agent
### Important Context
The base setup is DONE and verified — do NOT re-run destructively or reconfigure. `scripts/run.sh` is idempotent and safe to re-run (it re-installs/removes the temp sudoers drop-in each time). Everything left is Seth's GUI checklist + Ableton install. If `ssh mac` fails, first suspect a reboot (FileVault locks SSH until password entry at the lid), not a config problem. Never leave `/etc/sudoers.d/mac-setup` behind — run.sh removes it three ways, but if a run is force-killed mid-way, verify with `ssh mac 'test -e /etc/sudoers.d/mac-setup && echo BAD || echo ok'`.
### Assumptions Made
- Seth is physically at the Mac (enabled Remote Login mid-session).
- `$HOMELAB_PASSWORD` is the Mac's login/sudo password (verified — it worked).
- The Mac stays on AC while producing (power profile only changes AC, not battery).
### Potential Gotchas
- `pkill -f 'ssh ... mac'` will kill your OWN session (matched my command line once — cost a retry).
- SSH is non-login: GNU tools/PATH need `source ~/.bash_profile`.
- A fast test passing does NOT mean it's reliable — the pty password inject passed a 20s test then hung 14 min live.
## Environment State
### Tools/Services Used
- `ssh mac` (192.168.0.94, key auth), `ssh pve173` (tank host). Homebrew at /opt/homebrew.
- backup launchd: `~/Library/LaunchAgents/xyz.sethpc.mac-backup.plist` (03:30 nightly).
### Active Processes
- Backup launchd agent loaded (idle until 03:30). sanoid.timer active on pve173.
- No leftover background jobs from this session (drop-in removed, self-destruct sleepers killed).
### Environment Variables
- `HOMELAB_PASSWORD` (in claude's env on steel141; used by run.sh and cleanup).
## Related Resources
- `docs/plans/2026-09-15-mac-setup-design.md` (spec), `docs/plans/2026-09-15-mac-setup-plan.md` (plan)
- `docs/manual-checklist.md` (remaining work), `docs/reference/inventory-2026-09-15.md` (baseline)
- `DECISIONS.md` (why), `~/bin/ableton/` (Wine attempt archive, sibling project)
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
@@ -0,0 +1,121 @@
# Handoff: kitty titlebar fix (mac)
## Session Metadata
- Created: 2026-09-15 20:03:54
- Project: /home/claude/bin/mac
- Branch: master
- Session duration: ~10 min
### Recent Commits (for context)
- 3399829 fix(kitty): keep macOS titlebar — it's the only drag handle; color it to match bg
- d86ede0 docs: session handoff — mac base setup shipped & verified
- 617fc78 docs: reconcile CLAUDE.md/DECISIONS with what shipped (sudo approach, rrsync, set -e lessons)
- d622cf6 fix: chmod 600 the ssh Include file (ssh rejects group-writable config)
- d7205a9 fix: pass pubkey to tank-side.sh via env, not ssh command arg
## Handoff Chain
- **Continues from**: [2026-09-15-194209-mac-setup-shipped.md](./2026-09-15-194209-mac-setup-shipped.md)
- Previous title: kitty titlebar fix (mac)
- **Supersedes**: [list any older handoffs this replaces, or "None"]
> Review the previous handoff for full context before filling this one.
## Current State Summary
Short follow-up to the base-setup session. Seth reported kitty on the MacBook had no title bar and no way to move the window. Root cause: `config/kitty.conf` carried `hide_window_decorations titlebar-only` (from the plan) plus a leftover `hide_window_decorations yes` inherited verbatim from `~/bin/kitty-web/config/kitty.conf` (where Xpra provides the frame). On macOS the title bar is the only drag handle; Rectangle snaps but doesn't free-drag. Fixed by deleting the `yes` line and replacing `titlebar-only` with `macos_titlebar_color background` so the bar stays but is painted #0a0a0a. Synced to the Mac, committed, pushed. Seth still needs to Cmd+Q/relaunch kitty (decoration changes don't hot-reload). Everything from the previous handoff's Pending Work is still pending and unchanged.
## Codebase Understanding
### Architecture Overview
Unchanged from previous handoff. Relevant slice: `config/kitty.conf` in the repo is *included* by `~/.config/kitty/kitty.conf` on the Mac, and the repo is rsynced to `~/mac/` — so a one-file fix is `rsync -a config/kitty.conf mac:mac/config/kitty.conf` (no need for the full `run.sh`, which installs/removes the temp sudoers drop-in).
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `config/kitty.conf` | kitty config, included on the Mac | macOS section at the bottom; last-write-wins, so watch for Linux leftovers above it |
| `config/.backup/kitty.conf.*` | pre-edit copy | per global backup rule |
| `docs/manual-checklist.md` | Seth's GUI steps | still the remaining work |
### Key Patterns Discovered
- `config/kitty.conf` was copied from `~/bin/kitty-web/` — audit any other Linux/Xpra-specific lines before assuming they make sense on macOS.
- `kill -USR1 $(pgrep -x kitty)` reloads kitty config remotely, but window-decoration options only apply on relaunch.
## Work Completed
### Tasks Finished
- [x] Diagnosed missing titlebar -> `hide_window_decorations`
- [x] Fixed `config/kitty.conf`, rsynced to Mac, SIGUSR1 reload sent
- [x] Committed + pushed (`fix(kitty): keep macOS titlebar ...`)
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| [no modified files detected] | | |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Keep titlebar, paint it `background` | (a) `hide_window_decorations no` stock bar; (b) keep hidden + global `NSWindowShouldDragOnGesture` (Ctrl+Cmd+drag anywhere); (c) chosen | (c) keeps drag + traffic lights with a flat look; (b) is a global `defaults` change on the DAW box — untested against Live's modifier-drag shortcuts, so deferred |
## Pending Work
### Immediate Next Steps
1. Seth: Cmd+Q and relaunch kitty on the Mac; confirm the bar is there and matches the bg.
2. Resume previous handoff's list: hand Seth `docs/manual-checklist.md`, install Ableton Live 12, re-run `scripts/run.sh --no-tank` so the Dock picks up Live.
3. Audio interface + 15-min AC session test with DAW Focus on (see previous handoff).
### Blockers/Open Questions
- [ ] None new. Carried: S4 MK1 macOS controller support unverified; FileVault ON kills `ssh mac` after reboot until password typed at lid.
### Deferred Items
- `defaults write -g NSWindowShouldDragOnGesture -bool true` (Linux Alt+drag equivalent) — only if Seth wants the bar gone again; global, so verify against Live first and add it to `setup.sh` with a defaults backup, not by hand.
- Carried from previous: SparkFun PPP service, Tailscale GUI login, AeroSpace tiling.
## Context for Resuming Agent
### Important Context
This session changed ONE repo file and nothing else on the Mac. Read `2026-09-15-194209-mac-setup-shipped.md` for the real state of the machine — base setup is DONE and verified; do not reconfigure. If Seth reports kitty still has no bar, he hasn't relaunched it yet (reload via SIGUSR1 does not apply decoration changes). If `ssh mac` fails, suspect a FileVault reboot first.
### Assumptions Made
- kitty 0.48.2 on the Mac honours `macos_titlebar_color background` (option has existed for years; not visually verified from steel141).
- Seth wants a movable window more than a bar-less look (he reported the missing bar as a problem, not the bar as noise).
### Potential Gotchas
- kitty config is last-write-wins: a Linux-section `hide_window_decorations yes` silently overrides nothing only because a later macOS line existed. Check for other duplicated keys before editing.
- Don't run full `run.sh` for a config-only tweak; rsync the file.
## Environment State
### Tools/Services Used
- `ssh mac` (192.168.0.94, key auth). kitty 0.48.2 via Homebrew cask. `gitea push`.
### Active Processes
- kitty running on the Mac (needs relaunch). Backup launchd agent + sanoid unchanged from previous handoff.
### Environment Variables
- None used this session. (`HOMELAB_PASSWORD` still needed for `run.sh`.)
## Related Resources
- Previous handoff: `.claude/handoffs/2026-09-15-194209-mac-setup-shipped.md` (authoritative for machine state)
- `DECISIONS.md`, `docs/manual-checklist.md`, `docs/plans/2026-09-15-mac-setup-plan.md` (Task 1 step 3 is where `titlebar-only` came from)
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
@@ -0,0 +1,135 @@
# Handoff: ssh aliases + two-key split (mac)
## Session Metadata
- Created: 2026-09-15 20:31:33
- Project: /home/claude/bin/mac
- Branch: master
- Session duration: ~30 min
### Recent Commits (for context)
- 2028508 feat(ssh): full homelab alias set on the Mac + separate interactive key
- 43877eb docs(checklist): kitty needs the Local Network grant — else LAN ssh = No route to host
- dc29b7a docs: session handoff — kitty titlebar fix
- 3399829 fix(kitty): keep macOS titlebar — it's the only drag handle; color it to match bg
- d86ede0 docs: session handoff — mac base setup shipped & verified
## Handoff Chain
- **Continues from**: [2026-09-15-200354-kitty-titlebar-fix.md](./2026-09-15-200354-kitty-titlebar-fix.md)
- Previous title: kitty titlebar fix (mac)
- **Supersedes**: None
> Review the previous handoff for full context before filling this one.
## Current State Summary
Seth asked for ssh aliases on the Mac. `config/ssh_homelab` already existed with 6 hosts, but the Mac's only key (`id_ed25519`) is rrsync-jailed on pve173, so `ssh pve173` would have hit the jail and nothing else was authorized. Shipped: the curated alias set from `~/bin/CLAUDE.md` (pve nodes, machines, key CTs, switch, tailscale hosts), a second key `id_ed25519_homelab` for interactive use (`Host *` default), `backup.sh` pinned to the jailed key, and `scripts/authorize-mac-key.sh` (runs on steel141, pushes the interactive pubkey via claude's aliases, wired into `run.sh`). Applied without a full `run.sh`: rsync + keygen over ssh + the authorize script. 12 aliases verified from the Mac as the right user@host; jail proven with `id`; `backup.sh` ran clean. Five hosts were offline and did not get the key. Committed + pushed (`2028508`).
## Codebase Understanding
### Architecture Overview
- `~/.ssh/config` on the Mac is ONE line: `Include ~/mac/config/ssh_homelab`. The repo file is the whole config; `Host *` at its bottom is the default block (first-obtained-wins semantics).
- Two-key design: `id_ed25519` (seth@mac) = backup only, jailed by `restrict,command="/usr/bin/rrsync /tank/backups/mac"` on pve173 line 19; `id_ed25519_homelab` = interactive, unrestricted everywhere. sshd matches the FIRST authorized_keys line per key, so a single key cannot serve both roles on pve173.
- Proxmox nodes share `/etc/pve/priv/authorized_keys` (root's `~/.ssh/authorized_keys` is a symlink) — one append covers all four nodes; that's why the push printed `[set] pve112` then `[skip]` for the rest.
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `config/ssh_homelab` | all aliases + `Host *` defaults | edit here, then `rsync -a config/ssh_homelab mac:mac/config/`; must stay mode 600 (ssh refuses a group-writable Include) |
| `scripts/authorize-mac-key.sh` | pushes the interactive pubkey to LAN hosts from steel141 | idempotent; re-run for hosts that were offline |
| `scripts/backup.sh` | nightly rsync | `-o IdentitiesOnly=yes -i $HOME/.ssh/id_ed25519` pins the jailed key; do not drop it |
| `scripts/setup.sh` | generates both keys | `[skip]` on rerun |
| `DECISIONS.md` | two-key rationale, alias-set scope, `bedroom`=root | |
### Key Patterns Discovered
- Config-only tweaks: rsync the file, don't run `run.sh` (it installs/removes a temp sudoers drop-in).
- Remote one-liners take data via env in the command string (`PUB='$PUB' H=$H bash -s` with the script on stdin) — same trick as `tank-side.sh`; avoids ssh re-splitting the key line.
- `rrsync` whitelists `true` as a connectivity ping (`if command == 'true': sys.exit(0)`). A jail check must use `id`, not `true``true` returning 0 looked like a broken jail and cost a diagnosis loop.
## Work Completed
### Tasks Finished
- [x] `config/ssh_homelab`: full curated alias set, `Host *` -> `id_ed25519_homelab`, `StrictHostKeyChecking accept-new`
- [x] `id_ed25519_homelab` generated on the Mac (setup.sh now does this; ran the same keygen by hand)
- [x] `backup.sh` pins the jailed key; verified jail (`id` refused) and backup run (exit 0, `Documents` on tank)
- [x] `scripts/authorize-mac-key.sh` written, run: key on all 4 pve nodes, vdj-rig, caddy, rtorrent, emby, mcsmanager, pihole, arr, steel141 (seth)
- [x] 12 aliases verified from the Mac (`ssh <alias> 'whoami@hostname'`)
- [x] DECISIONS.md + CLAUDE.md updated; committed + pushed
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| [no modified files detected] | | |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Second key rather than re-keying the backup | (a) rename existing key to `_backup` + regenerate default; (b) add `id_ed25519_homelab` | (b): no migration, no change to `run.sh`'s tank-side push, server side untouched |
| `bedroom` alias user `seth` -> `root` | keep `seth` (previous session, undocumented) | matches steel141's alias + CLAUDE.md table; avoids a sudo special-case in the push. One-word revert if Seth wants `seth` |
| Alias scope = CLAUDE.md curated table, not claude's full `~/.ssh/config` | mirror everything | nixos/thinkpad-nano/mortdec-auth/vast are stale or claude-only |
| `StrictHostKeyChecking accept-new` (not `no`) | `no` like steel141 | TOFU still detects later key changes; same zero-prompt first connect |
## Pending Work
### Immediate Next Steps
1. When bedroom / thinkcentre / seth-pi / pinail / openclaw2 are powered on: `scripts/authorize-mac-key.sh` from steel141 (idempotent, prints `[set]`/`[skip]`/`[FAIL]` per host).
2. Confirm with Seth that `bedroom` as root is what he wants from the Mac.
3. Carried from previous handoffs: Seth relaunches kitty; `docs/manual-checklist.md`; install Ableton Live 12 then `scripts/run.sh --no-tank`; audio interface + AC session test.
### Blockers/Open Questions
- [ ] yoga / bebop aliases are dead until Tailscale is logged in on the Mac (manual-checklist item); their authorized_keys were not touched.
- [ ] `switch` alias is password-only by design (TP-Link); needs `sshpass` on the Mac if Seth wants it non-interactive (not installed).
### Deferred Items
- Gitea ssh access from the Mac (`git@git.sethpc.xyz`) — not asked; would need the homelab pubkey added in Gitea, not authorized_keys.
- Keys for rosy / matt-strix — other people's boxes, not Seth's call to make from here; aliases intentionally omitted.
- Carried: SparkFun PPP service, Tailscale GUI login, AeroSpace, `NSWindowShouldDragOnGesture`.
## Context for Resuming Agent
### Important Context
Do NOT collapse the two keys back into one. If `ssh pve173` from the Mac ever lands in `rrsync error: ... does not run rsync`, the homelab key is missing from pve173's authorized_keys — run `authorize-mac-key.sh`, don't touch the jailed line. If `backup.sh` ever gets an unjailed root shell (test: `ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.0.173 id` must be REFUSED), the `-i` pin in backup.sh was lost. `true` is NOT a valid jail test — rrsync allows it. This session's machine changes: one new key on the Mac, `config/ssh_homelab` + `scripts/backup.sh` rsynced, pubkey appended on the hosts listed above, seth@steel141 authorized_keys appended via sudo.
### Assumptions Made
- Seth wants root on pve nodes/CTs and his own user on his machines (mirrors how claude's aliases are documented).
- The five offline hosts are just powered down, not decommissioned (bedroom/thinkcentre/seth-pi/pinail/openclaw2 all still in CLAUDE.md).
### Potential Gotchas
- `rsync -a` carries file mode: `config/ssh_homelab` must be 600 locally or the Mac's ssh silently ignores the Include (`Bad owner or permissions`).
- `authorize-mac-key.sh` uses claude's steel141 aliases, which have the SAME names as the Mac's — easy to confuse which side you're on. It only runs from steel141.
- `sudo -u seth bash -c` sets HOME to /home/seth on steel141 (verified) — `~/.ssh` in the shared one-liner resolves correctly.
## Environment State
### Tools/Services Used
- `ssh mac` (192.168.0.94, key auth). OpenSSH 10.0p2 on the Mac. `gitea push`. `sudo -n` on steel141 (passwordless, used for seth's authorized_keys).
### Active Processes
- Nothing new. Backup launchd agent unchanged (next run 03:30 uses the pinned key — first unattended run with the new `-e` string; check `/tank/backups/mac` next day).
### Environment Variables
- None used. (`HOMELAB_PASSWORD` still required for `run.sh`; `authorize-mac-key.sh` needs none.)
## Related Resources
- Previous handoff: `.claude/handoffs/2026-09-15-200354-kitty-titlebar-fix.md` (kitty relaunch still pending) and `2026-09-15-194209-mac-setup-shipped.md` (machine state).
- `DECISIONS.md` (two-key entry), `~/bin/CLAUDE.md` ssh alias table (source of the alias set), `scripts/tank-side.sh` (how the jail line is written).
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
@@ -0,0 +1,137 @@
# Handoff: tank automount on the Mac (autofs + SMB)
## Session Metadata
- Created: 2026-09-15 22:24:55
- Project: /home/claude/bin/mac
- Branch: master
- Session duration: ~35 min
### Recent Commits (for context)
- c1558d2 feat(tank): autofs SMB mount of tank at /Volumes/tank
- c204d99 docs: session handoff — ssh aliases + two-key split
- 2028508 feat(ssh): full homelab alias set on the Mac + separate interactive key
- 43877eb docs(checklist): kitty needs the Local Network grant — else LAN ssh = No route to host
- dc29b7a docs: session handoff — kitty titlebar fix
## Handoff Chain
- **Continues from**: [2026-09-15-203133-mac-ssh-aliases.md](./2026-09-15-203133-mac-ssh-aliases.md)
- Previous title: ssh aliases + two-key split (mac)
- **Supersedes**: None
> Review the previous handoff for full context before filling this one.
## Current State Summary
Seth asked to "mount tank on the mac as a drive". Shipped `scripts/tank-automount.sh`: an autofs direct map (`/- auto_smb -nosuid` in `/etc/auto_master`) + root-owned 0600 `/etc/auto_smb` mounting `//Administrator@192.168.0.173/tank` (soft) at `/Volumes/tank`. Mounts on access, unmounts when idle — chosen over a Finder Login-Item mount because the laptop roams. Verified: mount works, writes land as root on tank, two consecutive all-`[skip]` reruns, wired into `run.sh`. The literal "as a drive" is NOT achievable this way: macOS marks all automounted fs `nobrowse` (confirmed in `man auto_master`), so it shows as a folder and needs a Finder Favorite (added to `docs/manual-checklist.md`). A Finder window on `/Volumes/tank` was opened on Seth's screen. Not yet verified across a reboot (FileVault means Seth must be at the lid). Committed + pushed.
## Codebase Understanding
### Architecture Overview
- `run.sh` now has 3 on-Mac stages: `setup.sh` (temp NOPASSWD sudoers), `tank-automount.sh` (own `sudo -S -v` from the stdin password — needs no temp sudoers), then tank-side + `authorize-mac-key.sh` from steel141.
- tank export facts (pve173): NFS `/tank *(rw,no_root_squash,insecure)`; Samba shares `[tank]` and `[Z]` (same path, `force user = root`, users `Administrator`/`octoprint`). steel141's seth uses `//173/Z` as Administrator; vdj-rig uses NFS ro with a systemd automount — the roaming pattern this copies.
- The Mac's `seth` is uid 501, which is why NFS was rejected: writes would be an unknown uid and only 777 dirs are writable.
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `scripts/tank-automount.sh` | the whole feature; password on stdin | idempotent; see header comment for the three macOS gotchas |
| `/etc/auto_smb` (on the Mac) | contains the SMB password, root 0600 | never copy into the repo or `.mac-setup-backup` |
| `/etc/auto_master` (on the Mac) | direct-map line appended | pre-change copy in `~/.mac-setup-backup/auto_master-<ts>` on the Mac |
| `docs/manual-checklist.md` | new "tank" section + fixed Gitea key + samples step | Seth's next GUI actions |
| `DECISIONS.md` | autofs-vs-Finder-mount rationale; Finder mount in Deferred/Rejected | |
### Key Patterns Discovered
- macOS `automount -vc` does NOT create direct-map trigger dirs ("mountpoint unavailable", `getattrlist:fail:2`). `mkdir -p` first.
- No-tty sudo ticket (`sudo -S -v` over ssh) is per parent pid: `sudo` in a pipeline or as a direct child works; `$(sudo cat ...)` silently fails. Cost a false "not idempotent".
- `nobrowse` in a map's options and `nobrowse` on the resulting mount are different things (man page says so); there is no map option that makes an automount Finder-browsable.
- Secrets to on-Mac scripts go via stdin (`printf | ssh mac 'bash script'`), never argv/env in the ssh command string; URL-encoding the password also went through python3 stdin, not argv.
## Work Completed
### Tasks Finished
- [x] `scripts/tank-automount.sh` written, applied, verified (mount, root write, idempotent reruns)
- [x] `run.sh` runs it after `setup.sh`
- [x] `docs/manual-checklist.md`: tank Favorite + reboot check; Gitea key -> `id_ed25519_homelab`; samples step now uses `/Volumes/tank`
- [x] DECISIONS.md + CLAUDE.md updated; committed + pushed
- [x] ~~Finder window opened on `/Volumes/tank` for Seth~~`open` over ssh did nothing visible; Seth given the Cmd+Shift+G path instead
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| [no modified files detected] | | |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| autofs (roaming-safe, folder) over Finder Login-Item mount (drive icon, hard mount) | both; a custom LaunchAgent doing `mount_smbfs` was the third (rung-7) option | laptop leaves the LAN; hard mounts beachball Finder and throw login dialogs. Icon can be revisited |
| SMB as Administrator over NFS | NFS is passwordless | uid 501 on the Mac; SMB `force user = root` matches how seth writes tank from steel141 |
| `/Volumes/tank` as the path | `~/tank`, `/tank` via synthetic.conf | reads as a drive path; persistence across reboot is the open question — fallback is synthetic.conf `/tank` (needs a reboot) |
| Password in `/etc/auto_smb` root 0600 | keychain (not honoured by automountd) | same class as steel141's `~/.smbcredentials`; never leaves the Mac |
## Pending Work
### Immediate Next Steps
1. After Seth's next reboot: `ssh mac 'mount | grep auto_smb'` must show the `/Volumes/tank` trigger. If `/Volumes/tank` vanished (boot-time /Volumes cleanup), switch the map to `/tank` via `/etc/synthetic.conf` (`tank` bare line) — needs another reboot.
2. Seth: Cmd+Shift+G -> `/Volumes/tank`, drag `tank` from the window title into Favorites (checklist). He was told this at 22:40; not yet confirmed done. Ask whether the folder-not-drive result is acceptable or he wants the Locations icon (then: Finder Connect-to-Server + Login Item, documented as rejected-for-now in DECISIONS).
3. Carried: `authorize-mac-key.sh` rerun for bedroom/thinkcentre/seth-pi/pinail/openclaw2; `bedroom`=root confirmation; kitty relaunch; Ableton install then `run.sh --no-tank`; audio interface test.
### Blockers/Open Questions
- [ ] Reboot persistence of the `/Volumes/tank` trigger dir — unverified (FileVault: needs Seth at the lid).
- [ ] Idle-unmount timing not measured (macOS default `AUTOMOUNT_TIMEOUT` in `/etc/autofs.conf`, 3600 s); fine unless Seth notices.
### Deferred Items
- Real drive icon under Locations (Finder mount + Login Item) — rejected for now, see DECISIONS.
- LaunchAgent-based `mount_smbfs` reconnect daemon — would give icon + roaming, but it's custom code for a problem autofs solves well enough.
- Carried: SparkFun PPP, Tailscale login, AeroSpace, `NSWindowShouldDragOnGesture`, Gitea key upload.
## Context for Resuming Agent
### Important Context
`/etc/auto_smb` on the Mac holds the homelab password — never `cat` it into a transcript, never back it up into the repo or `~/.mac-setup-backup`. If tank "isn't mounting": `ls /Volumes/tank` is what triggers it (autofs is lazy); check `mount | grep tank` for the autofs trigger line first — if the trigger is missing, `automount -vc` failed (usually the dir is gone); if the trigger is there but smbfs isn't, it's network/creds (`log show --last 2m --predicate 'process == "automountd"'`). Re-running `printf '%s\n' "$HOMELAB_PASSWORD" | ssh mac 'bash ~/mac/scripts/tank-automount.sh'` is always safe. This session changed on the Mac: `/etc/auto_master` (+1 line), `/etc/auto_smb` (new), `/Volumes/tank` (dir), and opened one Finder window; nothing else.
### Assumptions Made
- Seth's Mac sudo password == `$HOMELAB_PASSWORD` (true: run.sh's cleanup relies on it) and the Samba `Administrator` password == the same (verified by the mount succeeding).
- Seth values roaming-safety over the drive icon (inferred from "portable" use; not confirmed — item 2 in next steps).
### Potential Gotchas
- `/Volumes` has the `UF_HIDDEN` flag on every Mac — Finder never shows it when browsing Macintosh HD, and `/System/Volumes` (APFS Data/Preboot/VM) is unrelated. Seth hit exactly this ("I don't even see a /Volumes folder"). Path in: Cmd+Shift+G -> `/Volumes/tank`; in kitty `ls /Volumes/tank`.
- `open /Volumes/tank` over ssh returned success but no Finder window appeared for Seth. Don't use `open` from ssh as a "show it to Seth" signal; tell him the path instead.
- The Ableton Live 12 Trial Installer DMG was mounted at `/Volumes/Ableton Live 12 Trial Installer` during this session — Seth is mid-install; don't unmount/eject volumes wholesale.
- `mount | grep tank` shows TWO lines (autofs trigger + smbfs) when mounted; only the trigger when idle. Both are normal.
- `sed -i ""` (BSD form) is what works on the Mac even with GNU sed on PATH via gnubin — check `which sed` before scripting sed there.
## Environment State
### Tools/Services Used
- `ssh mac`; `/usr/sbin/automount`, `/usr/libexec/automountd`; `/usr/bin/python3` (Xcode CLT) used for URL-encoding; `gitea push`.
### Active Processes
- tank currently mounted at `/Volumes/tank` on the Mac (will idle-unmount). Finder window open on it. Backup launchd agent unchanged.
### Environment Variables
- `HOMELAB_PASSWORD` — piped on stdin to `tank-automount.sh` (and by `run.sh`); never in argv.
## Related Resources
- Previous handoff: `.claude/handoffs/2026-09-15-203133-mac-ssh-aliases.md` (two-key ssh design, offline hosts list).
- `man auto_master` on the Mac (nobrowse semantics); `/etc/autofs.conf` (timeouts); `~/bin/CLAUDE.md` Storage section (tank export facts, use .173 not .200).
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
+12 -5
View File
@@ -21,16 +21,21 @@ laptop" shape).
## Current State ## Current State
- **Phase:** ideation -> design approved-ish; spec at `docs/plans/2026-09-15-mac-setup-design.md` - **Phase:** shipping — `scripts/run.sh` applied & verified 2026-09-15. Manual checklist (`docs/manual-checklist.md`) pending Seth; Ableton not yet installed.
- **Repo:** `git.sethpc.xyz/Seth/mac` - **Repo:** `git.sethpc.xyz/Seth/mac`
- **Deploy target:** the MacBook, `ssh mac` (192.168.0.94, user seth, en0 Wi-Fi) - **Deploy target:** the MacBook, `ssh mac` (192.168.0.94, user seth, en0 Wi-Fi)
- **Hardware:** MacBookPro18,1 (16" 2021), M1 Pro, 32 GB, 512 GB. **macOS 26.2 Tahoe.** - **Hardware:** MacBookPro18,1 (16" 2021), M1 Pro, 32 GB, 512 GB. **macOS 26.2 Tahoe.**
- Remote Login enabled + claude key installed 2026-09-15. `ssh mac` works from steel141. - Remote Login enabled + claude key installed 2026-09-15. `ssh mac` works from steel141.
- **FileVault is on** — no SSH after a reboot until the password is typed at the lid. - **FileVault is on** — no SSH after a reboot until the password is typed at the lid.
- Stock state is already clean: no Homebrew/CLT, only Safari in /Applications, - Applied: Homebrew + Brewfile (GNU userland, kitty, rectangle, tailscale-app),
Siri off, Apple Intelligence opted out. Full snapshot: bash5 login shell, hostname `mac`, Linux-feel + debloat `defaults`, Dock=2 apps,
`docs/reference/inventory-2026-09-15.md` — read when checking "was X stock?". DAW power profile (AC sleep 0 / powernap 0), nightly rsync backup agent.
- No changes made yet. Pre-change inventory: `docs/reference/inventory-2026-09-15.md`.
- Backup: `~/Music/Ableton` + `~/Documents` -> `pve173:/tank/backups/mac` nightly
03:30 via launchd; key is rrsync-jailed; sanoid keeps history (tank_media template).
- ssh aliases (`config/ssh_homelab`, included from `~/.ssh/config`) mirror the `~/bin/CLAUDE.md` set. Two keys: `id_ed25519` = backup (rrsync-jailed on pve173), `id_ed25519_homelab` = interactive. `scripts/authorize-mac-key.sh` (from steel141) pushes the interactive key; re-run it when an offline host comes back.
- tank: autofs SMB at `/Volumes/tank` (`scripts/tank-automount.sh`, password on stdin). Mounts on access, `nobrowse` by macOS design → a Finder Favorite, not a Locations drive. Not yet verified across a reboot.
- SparkFun PPP service left in place (macOS won't remove the sole service on a port; harmless).
## Conventions ## Conventions
@@ -40,3 +45,5 @@ laptop" shape).
- Before any `launchctl disable`/`bootout`: record current `launchctl print` state to `.backup/` - Before any `launchctl disable`/`bootout`: record current `launchctl print` state to `.backup/`
- Homebrew `Brewfile` at repo root is the package manifest — `brew bundle` is the install - Homebrew `Brewfile` at repo root is the package manifest — `brew bundle` is the install
- macOS gotchas go in this file's Conventions; per-decision reasoning goes in `DECISIONS.md` - macOS gotchas go in this file's Conventions; per-decision reasoning goes in `DECISIONS.md`
- **Apply everything:** `scripts/run.sh` from steel141. It installs a TEMPORARY `/etc/sudoers.d/mac-setup` (NOPASSWD) via `_install_sudoers.sh` for the run and removes it on every exit path (+40-min on-Mac self-destruct backstop). Needs `$HOMELAB_PASSWORD`. Second run is all `[skip]`.
- GUI-only steps live in `docs/manual-checklist.md` — read when something "didn't apply" (it's probably on that list).
+9
View File
@@ -16,10 +16,19 @@ Format: `YYYY-MM-DD: <decision> — <why>`
- 2026-09-15: Spotlight indexing stays on, Siri/web suggestions off — Finder search is useful; Live's browser has its own index. - 2026-09-15: Spotlight indexing stays on, Siri/web suggestions off — Finder search is useful; Live's browser has its own index.
- 2026-09-15: iCloud account stays, iCloud Drive Desktop&Documents sync OFF — cloud sync mid-save is a DAW project killer. - 2026-09-15: iCloud account stays, iCloud Drive Desktop&Documents sync OFF — cloud sync mid-save is a DAW project killer.
- 2026-09-15: **Unattended sudo = temporary `/etc/sudoers.d/mac-setup` (NOPASSWD), not pty password injection.** Piping the password into `ssh -tt` raced and hung 14 min on the real run (passed a fast test). The drop-in is validated before activation, removed on every run.sh exit path, and self-destructs on the Mac after 40 min. See run.sh / _install_sudoers.sh.
- 2026-09-15: Mac's backup key on pve173 is **rrsync-restricted** (`command="/usr/bin/rrsync /tank/backups/mac"`) — a travelling laptop's key must never be unrestricted root on the tank host. Proven: an interactive command over that key is refused.
- 2026-09-15: **Optional/cosmetic steps must be non-fatal under `set -e`.** The SparkFun removal (macOS refuses) and the GUI-domain `launchctl bootstrap` (flaky over SSH) both aborted the run until guarded with `|| warn`. Rule for this repo: anything that can legitimately fail without breaking the goal gets a best-effort guard.
- 2026-09-15: **Two ssh keys on the Mac: `id_ed25519` (backup, rrsync-jailed) + `id_ed25519_homelab` (interactive, `Host *` default).** sshd honours the first matching authorized_keys line per key, so one key can't be both jailed and a shell on pve173. `backup.sh` pins the jailed key with `-i` + `IdentitiesOnly`; verify with `id`, not `true` (rrsync whitelists `true` as a ping). Key push = `scripts/authorize-mac-key.sh` from steel141 (uses claude's aliases; pve nodes share `/etc/pve/priv/authorized_keys`, so one write covers all four).
- 2026-09-15: Mac ssh aliases mirror the curated set in `~/bin/CLAUDE.md` (not claude's full `~/.ssh/config`) — stale/undocumented entries (nixos, thinkpad-nano, mortdec-auth, vast) left out. `bedroom` = root, matching steel141's alias (was `seth`, undocumented).
- 2026-09-15: **tank on the Mac = autofs + SMB (`Administrator`, = root on tank) at `/Volumes/tank`, via `scripts/tank-automount.sh`** — mounts on access, unmounts idle, `soft`: a laptop that leaves the LAN gets no hung Finder and no login-time "server unavailable" dialogs (same idea as vdj-rig's `x-systemd.automount`). SMB over NFS because the Mac's seth is uid 501: NFS would write as an unknown uid and only into the 777 dirs; SMB `force user = root` matches steel141's `/mnt/Z`. Cost: the password sits in root-owned `/etc/auto_smb` (0600) — same class as steel141's `~/.smbcredentials`. `automount` does not create direct-map trigger dirs, so the script `mkdir -p`s it. sudo inside `$(...)` loses the no-tty ticket — keep sudo in pipelines/direct calls.
- 2026-09-15: `defaults displaysleep` on AC = 30 min, not 0 — display sleep doesn't stop Live's audio; only system `sleep` does.
## Deferred / Rejected ## Deferred / Rejected
<!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas --> <!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas -->
- 2026-09-15: Cmd<->Ctrl global swap REJECTED — Live 12 and every Mac tutorial are Cmd-native; kitty already handles Ctrl+Shift+C/V. Karabiner per-app remaps are the fallback, not a global swap. - 2026-09-15: Cmd<->Ctrl global swap REJECTED — Live 12 and every Mac tutorial are Cmd-native; kitty already handles Ctrl+Shift+C/V. Karabiner per-app remaps are the fallback, not a global swap.
- 2026-09-15: yabai REJECTED — useful features need partial SIP disable. SIP/Gatekeeper/FileVault all stay on. - 2026-09-15: yabai REJECTED — useful features need partial SIP disable. SIP/Gatekeeper/FileVault all stay on.
- 2026-09-15: Time Machine REJECTED — over SMB it's fragile and whole-disk; rsync + tank-side ZFS snapshot covers the actual need. - 2026-09-15: Time Machine REJECTED — over SMB it's fragile and whole-disk; rsync + tank-side ZFS snapshot covers the actual need.
- 2026-09-15: Removing `/System/Applications` apps REJECTED — sealed system volume; hide from Dock instead. - 2026-09-15: Removing `/System/Applications` apps REJECTED — sealed system volume; hide from Dock instead.
- 2026-09-15: Finder "Connect to Server" + Login Item for tank REJECTED (for now) — it's the only way to get a real drive icon under Locations, but it hard-mounts: off-LAN logins throw a server dialog and a dropped network beachballs Finder for ~60 s. autofs mounts are always `nobrowse` (`man auto_master`), so the trade is drive-icon vs roaming-safe; roaming-safe won. Revisit if Seth wants the icon.
- 2026-09-15: Samples/projects on the SMB share REJECTED — local disk only; SMB is a copy source. External SSD is the capacity upgrade path. - 2026-09-15: Samples/projects on the SMB share REJECTED — local disk only; SMB is a copy source. External SSD is the capacity upgrade path.
+1 -2
View File
@@ -29,7 +29,6 @@ tab_title_max_length 20
# Window layout - no wasted space # Window layout - no wasted space
window_padding_width 2 window_padding_width 2
hide_window_decorations yes
placement_strategy top-left placement_strategy top-left
# Scrollback # Scrollback
@@ -77,5 +76,5 @@ sync_to_monitor no
# --- macOS --- # --- macOS ---
macos_option_as_alt yes macos_option_as_alt yes
hide_window_decorations titlebar-only macos_titlebar_color background # keep the titlebar (only drag handle on macOS), just paint it to match
macos_quit_when_last_window_closed yes macos_quit_when_last_window_closed yes
+60 -3
View File
@@ -1,5 +1,10 @@
# Included from ~/.ssh/config. Key ~/.ssh/id_ed25519 is generated by setup.sh; # Included from ~/.ssh/config (setup.sh adds the Include line). Two keys, both made by setup.sh:
# only pve173 is authorized so far (rrsync-restricted, for backup.sh). # ~/.ssh/id_ed25519_homelab interactive — authorized on the hosts below by scripts/authorize-mac-key.sh
# ~/.ssh/id_ed25519 backup only — rrsync-jailed on pve173; backup.sh pins it with -i
# They must stay separate: sshd honours the FIRST matching authorized_keys line per key, so one key
# can't be both jailed (nightly rsync) and unrestricted (a shell) on pve173.
# --- Proxmox nodes ---
Host pve173 Host pve173
HostName 192.168.0.173 HostName 192.168.0.173
User root User root
@@ -12,12 +17,64 @@ Host pve197
Host pve241 Host pve241
HostName 192.168.0.241 HostName 192.168.0.241
User root User root
# --- Machines ---
Host steel141 Host steel141
HostName 192.168.0.141 HostName 192.168.0.141
User seth User seth
Host bedroom Host bedroom
HostName 192.168.0.235 HostName 192.168.0.235
User root
Host vdj-rig
HostName 192.168.0.143
User seth User seth
Host thinkcentre
HostName 192.168.0.211
User seth
Host seth-pi
HostName 192.168.0.102
User seth
Host pinail
HostName 192.168.0.203
User pi
# TP-Link switch: password only (no pubkey support) — $HOMELAB_PASSWORD
Host switch
HostName 192.168.0.250
User admin
PubkeyAuthentication no
# --- Key CTs ---
Host caddy
HostName 192.168.0.185
User root
Host openclaw2
HostName 192.168.0.175
User root
Host rtorrent
HostName 192.168.0.213
User root
Host emby
HostName 192.168.0.215
User root
Host mcsmanager
HostName 192.168.0.244
User root
Host pihole
HostName 192.168.0.153
User root
Host arr
HostName 192.168.0.158
User root
# --- Tailscale-only (dead until Tailscale is logged in on the Mac — docs/manual-checklist.md) ---
Host yoga
HostName 100.113.203.99
User seth
Host bebop
HostName 100.78.2.106
User seth
Host * Host *
IdentityFile ~/.ssh/id_ed25519 IdentityFile ~/.ssh/id_ed25519_homelab
StrictHostKeyChecking accept-new
ServerAliveInterval 30 ServerAliveInterval 30
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>xyz.sethpc.mac-backup</string>
<key>ProgramArguments</key><array><string>/Users/seth/mac/scripts/backup.sh</string></array>
<key>StartCalendarInterval</key><dict><key>Hour</key><integer>3</integer><key>Minute</key><integer>30</integer></dict>
<key>StandardOutPath</key><string>/Users/seth/Library/Logs/mac-backup.log</string>
<key>StandardErrorPath</key><string>/Users/seth/Library/Logs/mac-backup.log</string>
</dict></plist>
+37
View File
@@ -0,0 +1,37 @@
# Manual checklist — GUI-only steps on macOS 26 Tahoe
Do these once, in order, after `scripts/run.sh` has completed. Tick and date.
## Apply the shell-level changes
- [ ] Log out / log in once (key repeat, press-and-hold, scroll direction only fully apply to a fresh session).
## Cloud + privacy
- [ ] System Settings > [your name] > iCloud > Drive (or "Saved to iCloud" > Drive): **Desktop & Documents Folders = OFF**. Keep account signed in.
- [ ] System Settings > General > AirDrop & Handoff: Handoff OFF, AirDrop = Contacts Only.
- [ ] System Settings > Screen Time: App & Website Activity OFF.
- [ ] System Settings > Spotlight: untick Siri Suggestions and any web/store result types; "Help Apple Improve Search" OFF.
- [ ] System Settings > Notifications: Allow notifications when mirroring/sharing OFF (keeps popups off a projector).
- [ ] System Settings > Network: delete the stray **SparkFun Pro Micro** service if present (setup.sh can't — macOS refuses to remove the sole service on a PPP port). Harmless if left.
## DAW Focus
- [ ] System Settings > Focus > "+" > Custom > name **DAW**, icon of your choice. Allowed notifications: none. "Share across devices" OFF. Toggle it from the menu-bar moon before a session.
## Apps needing a first-launch grant
- [ ] Launch **Rectangle** once -> grant Accessibility when prompted. Set your snap keys.
- [ ] Launch **kitty** once. Gatekeeper prompt -> Open. **Local Network prompt -> Allow** (or System Settings > Privacy & Security > Local Network > kitty ON). Symptom if missed: `ssh 192.168.0.x` says `No route to host` in kitty while Terminal works — macOS 15+ returns EHOSTUNREACH to LAN IPs for apps without the grant.
- [ ] Launch **Tailscale** -> log in to the tailnet. Confirm `tailscale status` in kitty and that `ssh mac` from bebop resolves via MagicDNS later.
- [ ] Gitea: `cat ~/.ssh/id_ed25519_homelab.pub` (the interactive key; `id_ed25519` is the rrsync-jailed backup key) -> https://git.sethpc.xyz/user/settings/keys -> Add Key. Then `git clone git@git.sethpc.xyz:Seth/mac.git` works from the Mac if you ever want to edit there.
## Ableton
- [ ] ableton.com > account > download Live 12 (Suite/trial) -> install to /Applications -> authorize.
- [ ] Re-run `scripts/run.sh --no-tank` from steel141 so the Dock picks up Live (the script only adds it if installed).
- [ ] Live > Browser > Packs: download the Suite packs you want (they're part of the license — do not torrent them).
- [ ] Third-party packs: copy from `/Volumes/tank/Downloads/Software/Milkie/` (tank automounts on access — see "tank" below) to `~/Music/Ableton/Samples/` (local disk — never run samples off SMB).
- [ ] Live > Settings > Audio: once an interface is attached, pick it, 48 kHz, buffer 128 (raise if crackle). Test with the DAW Focus on and the lid open on AC for 15 min: no sleep, no notification.
## tank
- [ ] Finder > **Cmd+Shift+G** > `/Volumes/tank` (this triggers the mount; `/Volumes` is a hidden folder — you won't find it by browsing Macintosh HD, and `/System/Volumes` is something else), then drag the `tank` folder from the path bar / window title into the sidebar **Favorites**. It will NOT appear under Locations as a drive: macOS marks every automounted share `nobrowse` (documented in `man auto_master`) — the Favorite is the one-click entry, and the share mounts on first click and unmounts when idle, so leaving the LAN never hangs Finder or throws login dialogs.
- [ ] After the first reboot: `mount | grep auto_smb` in kitty should show the `/Volumes/tank` trigger. If it's gone, `/Volumes` got cleaned at boot — tell Claude; the fix is a `synthetic.conf` mountpoint instead.
## Backup
- [ ] `ssh mac ~/mac/scripts/backup.sh` by hand once -> confirm `/tank/backups/mac/Ableton` appears on pve173 and `~/Library/Logs/mac-backup.log` exists.
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# Installs a TEMPORARY /etc/sudoers.d/mac-setup (NOPASSWD for seth) so an unattended setup run
# has sudo for Homebrew's long, child-spawning install. Reads the sudo password from stdin.
# Called by run.sh, which removes the drop-in afterward; a 40-min self-destruct is the backstop.
set -euo pipefail
read -r SUDO_PW
tmp=$(mktemp)
printf 'seth ALL=(ALL) NOPASSWD: ALL\n' > "$tmp"
sudo -S -p '' -v <<<"$SUDO_PW" # prime once from the piped password
sudo visudo -cf "$tmp" # validate OFF to the side (bad sudoers breaks sudo)
sudo install -m 440 -o root -g wheel "$tmp" /etc/sudoers.d/mac-setup
rm -f "$tmp"
nohup sudo bash -c 'sleep 2400; rm -f /etc/sudoers.d/mac-setup' >/dev/null 2>&1 & # survives ssh drop
echo "[set] temp NOPASSWD drop-in installed (+40m self-destruct)"
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# From steel141: put the Mac's interactive pubkey on every LAN host in config/ssh_homelab, via
# claude's own aliases (same names, same target users). Idempotent; called by run.sh, safe alone.
# steel141 itself is the one exception: seth's authorized_keys needs sudo from the claude account.
set -euo pipefail
PUB=$(ssh -o BatchMode=yes mac cat .ssh/id_ed25519_homelab.pub)
[[ $PUB == ssh-ed25519\ AAAA* ]] || { echo "no ~/.ssh/id_ed25519_homelab on the Mac — run setup.sh first"; exit 1; }
ADD='umask 077; mkdir -p ~/.ssh; if grep -qF "$PUB" ~/.ssh/authorized_keys 2>/dev/null; then echo "[skip] $H"; else echo "$PUB" >> ~/.ssh/authorized_keys; echo "[set] $H"; fi'
for H in pve112 pve173 pve197 pve241 bedroom vdj-rig thinkcentre seth-pi pinail caddy openclaw2 rtorrent emby mcsmanager pihole arr; do
ssh -o BatchMode=yes -o ConnectTimeout=5 "$H" "PUB='$PUB' H=$H bash -s" <<<"$ADD" || echo "[FAIL] $H"
done
sudo -n -u seth env PUB="$PUB" H=steel141 bash -c "$ADD"
+12
View File
@@ -0,0 +1,12 @@
#!/opt/homebrew/bin/bash
# Nightly by launchd (config/xyz.sethpc.mac-backup.plist). Mirrors the DAW-relevant
# dirs to tank. History comes from sanoid snapshots of tank/backups/mac on pve173,
# which is what makes `--delete` safe.
set -euo pipefail
src=()
for d in "$HOME/Music/Ableton" "$HOME/Documents"; do [[ -d $d ]] && src+=("$d"); done
[[ ${#src[@]} -gt 0 ]] || { echo "nothing to back up yet"; exit 0; }
# Remote path is relative to the rrsync root (/tank/backups/mac) set in root's authorized_keys on pve173.
# -i pins the jailed key; the Host * default (id_ed25519_homelab) has a root shell on pve173 and must NOT be offered first.
# Check the jail holds: ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.0.173 id -> "rrsync error: ... does not run rsync" (NOT `true`: rrsync whitelists it as a ping).
exec /opt/homebrew/bin/rsync -a --delete -e "ssh -o BatchMode=yes -o IdentitiesOnly=yes -i $HOME/.ssh/id_ed25519" "${src[@]}" root@192.168.0.173:/
Executable
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# From steel141: sync repo to the Mac, run setup there UNATTENDED, pull backups back, then tank side.
# Usage: scripts/run.sh [--no-tank]
#
# Homebrew's install needs sudo for 10+ min across child processes. We grant a TEMPORARY
# /etc/sudoers.d/mac-setup (NOPASSWD) for the run and guarantee its removal three ways:
# 1. explicit removal after setup, 2. an EXIT trap (covers failures / a dropped ssh from here),
# 3. a 40-min self-destruct on the Mac itself (covers steel141 losing the network).
# $HOMELAB_PASSWORD is used once (piped, never in argv) to install the drop-in.
set -euo pipefail
cd "$(dirname "$0")/.."
[[ -n ${HOMELAB_PASSWORD:-} ]] || { echo "HOMELAB_PASSWORD must be set"; exit 1; }
rsync -a --delete --exclude .git --exclude .backup --exclude Brewfile.lock.json ./ mac:mac/
cleanup(){ printf '%s\n' "$HOMELAB_PASSWORD" | ssh -o BatchMode=yes mac "sudo -S -p '' rm -f /etc/sudoers.d/mac-setup 2>/dev/null; sudo -K 2>/dev/null" || true; }
trap cleanup EXIT
# Install temp NOPASSWD drop-in (installer is a repo file, rsynced above; password piped to its stdin).
printf '%s\n' "$HOMELAB_PASSWORD" | ssh -o BatchMode=yes mac 'bash ~/mac/scripts/_install_sudoers.sh'
# sudo is now passwordless on the Mac; run setup unattended (Homebrew + everything).
ssh -o BatchMode=yes mac "HOSTNAME_WANT=mac ~/mac/scripts/setup.sh"
# tank autofs mount (reads the password on stdin: SMB credential + its own sudo -S).
printf '%s\n' "$HOMELAB_PASSWORD" | ssh -o BatchMode=yes mac 'bash ~/mac/scripts/tank-automount.sh'
# Explicit removal + confirm; then disarm the trap so it doesn't double-run.
cleanup; trap - EXIT
ssh -o BatchMode=yes mac 'test ! -e /etc/sudoers.d/mac-setup && echo "[ok] temp sudoers removed" || echo "[WARN] temp sudoers STILL PRESENT"'
mkdir -p .backup/mac && rsync -a mac:.mac-setup-backup/ .backup/mac/
scripts/authorize-mac-key.sh
[[ ${1:-} == --no-tank ]] && exit 0
PUB=$(ssh mac cat .ssh/id_ed25519.pub)
ssh pve173 "PUB='$PUB' bash -s" < scripts/tank-side.sh
echo "tank side done; test: ssh mac ~/mac/scripts/backup.sh"
+156
View File
@@ -0,0 +1,156 @@
#!/bin/bash
# setup.sh — run ON the Mac as seth, via scripts/run.sh from steel141.
# Idempotent: every section prints [skip] when already in the desired state.
set -euo pipefail
REPO="$HOME/mac"
BK="$HOME/.mac-setup-backup"; TS=$(date +%s); mkdir -p "$BK"
CHANGED=0
log(){ printf '\033[1;33m[%s]\033[0m %s\n' "$1" "$2"; }
# ---------- preflight ----------
[[ $(uname -m) == arm64 && $(sw_vers -productVersion) == 26.* ]] || { echo "not the Mac this was written for"; exit 1; }
[[ ${HOSTNAME_WANT:-} ]] || { echo "HOSTNAME_WANT=<name> required"; exit 1; }
sudo -n true 2>/dev/null || { echo "passwordless sudo unavailable — run via scripts/run.sh (it installs a temp drop-in)"; exit 1; }
# ---------- helpers ----------
backup_domain(){ # once per domain per run
local d=$1; local f="$BK/${d//\//_}-$TS.plist"
[[ -e $f || -e $f.absent ]] && return 0
defaults export "$d" "$f" 2>/dev/null || : > "$f.absent"
}
setd(){ # setd <domain> <key> <bool|int|string> <value>
local d=$1 k=$2 t=$3 v=$4 cur want=$4
[[ $t == bool ]] && { [[ $v == true ]] && want=1 || want=0; }
cur=$(defaults read "$d" "$k" 2>/dev/null || echo __unset__)
if [[ $cur == "$want" ]]; then log skip "$d $k=$v"; return 0; fi
backup_domain "$d"; defaults write "$d" "$k" "-$t" "$v"; log set "$d $k=$v"; CHANGED=1
}
install_marker(){ # install_marker <file> <line> — ensure a line exists in file (create if absent)
local f=$1 line=$2
if [[ -f $f ]] && grep -qxF "$line" "$f"; then log skip "$f has include"; return 0; fi
[[ -f $f ]] && cp "$f" "$BK/$(basename "$f")-$TS"
mkdir -p "$(dirname "$f")"; printf '%s\n' "$line" >> "$f"; log set "$f += $line"; CHANGED=1
}
# ---------- brew ----------
if [[ -x /opt/homebrew/bin/brew ]]; then log skip "homebrew installed"; else
log set "installing homebrew (installs Xcode CLT headless first; 5-15 min)"
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
CHANGED=1
fi
eval "$(/opt/homebrew/bin/brew shellenv)"
if brew bundle check --file="$REPO/Brewfile" >/dev/null 2>&1; then log skip "brew bundle satisfied"; else
log set "brew bundle"; brew bundle --file="$REPO/Brewfile"; CHANGED=1
fi
# ---------- shell ----------
grep -qx /opt/homebrew/bin/bash /etc/shells || { echo /opt/homebrew/bin/bash | sudo tee -a /etc/shells >/dev/null; log set "/etc/shells += homebrew bash"; }
if [[ $(dscl . -read "/Users/$USER" UserShell | awk '{print $2}') == /opt/homebrew/bin/bash ]]; then log skip "login shell bash5"; else
sudo chsh -s /opt/homebrew/bin/bash "$USER"; log set "login shell -> homebrew bash"; CHANGED=1
fi
install_marker "$HOME/.bash_profile" '[ -f ~/mac/config/bashrc ] && . ~/mac/config/bashrc # mac-setup'
install_marker "$HOME/.config/kitty/kitty.conf" "include $HOME/mac/config/kitty.conf"
# ---------- hostname ----------
for k in ComputerName LocalHostName HostName; do
if [[ $(scutil --get $k 2>/dev/null || true) == "$HOSTNAME_WANT" ]]; then log skip "$k=$HOSTNAME_WANT"; else
sudo scutil --set $k "$HOSTNAME_WANT"; log set "$k=$HOSTNAME_WANT"; CHANGED=1
fi
done
# ---------- ssh (homelab) ----------
mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
if [[ -f $HOME/.ssh/id_ed25519 ]]; then log skip "ssh key exists"; else
ssh-keygen -t ed25519 -N '' -C "seth@$HOSTNAME_WANT" -f "$HOME/.ssh/id_ed25519" >/dev/null; log set "generated ~/.ssh/id_ed25519"; CHANGED=1
fi
if [[ -f $HOME/.ssh/id_ed25519_homelab ]]; then log skip "ssh homelab key exists"; else # interactive key; see config/ssh_homelab header
ssh-keygen -t ed25519 -N '' -C "seth@$HOSTNAME_WANT-homelab" -f "$HOME/.ssh/id_ed25519_homelab" >/dev/null; log set "generated ~/.ssh/id_ed25519_homelab"; CHANGED=1
fi
if [[ -f $HOME/.ssh/config ]] && grep -q '^Include ~/mac/config/ssh_homelab' "$HOME/.ssh/config"; then log skip "ssh config include"; else
[[ -f $HOME/.ssh/config ]] && cp "$HOME/.ssh/config" "$BK/ssh_config-$TS"
{ echo 'Include ~/mac/config/ssh_homelab'; [[ -f $HOME/.ssh/config ]] && cat "$HOME/.ssh/config"; } > "$HOME/.ssh/config.new"
mv "$HOME/.ssh/config.new" "$HOME/.ssh/config"; chmod 600 "$HOME/.ssh/config"; log set "ssh config include"; CHANGED=1
fi
chmod 600 "$REPO/config/ssh_homelab" 2>/dev/null || true # ssh refuses a group/world-writable Include target
ssh-keygen -F 192.168.0.173 >/dev/null || { ssh-keyscan -t ed25519 192.168.0.173 >> "$HOME/.ssh/known_hosts" 2>/dev/null; log set "known_hosts += pve173"; }
# ---------- defaults: Linux feel ----------
G=NSGlobalDomain
setd $G KeyRepeat int 2
setd $G InitialKeyRepeat int 15
setd $G ApplePressAndHoldEnabled bool false # hold-key repeats instead of accent popup
setd $G com.apple.swipescrolldirection bool false # "natural" scrolling off
setd $G NSAutomaticWindowAnimationsEnabled bool false
setd $G AppleShowAllExtensions bool true
F=com.apple.finder
setd $F ShowPathbar bool true
setd $F ShowStatusBar bool true
setd $F _FXShowPosixPathInTitle bool true
setd $F FXPreferredViewStyle string Nlsv # list view
setd $F NewWindowTarget string PfHm # new windows open at ~
setd $F NewWindowTargetPath string "file://$HOME/"
setd com.apple.desktopservices DSDontWriteNetworkStores bool true
setd com.apple.desktopservices DSDontWriteUSBStores bool true
# ---------- defaults: debloat / Dock ----------
D=com.apple.dock
setd $D autohide bool true
setd $D autohide-delay int 0
setd $D show-recents bool false
setd $D tilesize int 48
for c in tl tr bl br; do setd $D wvous-$c-corner int 1; done # 1 = no action (stock br = Quick Note)
# Dock apps: only what exists. Finder + Trash are implicit.
dock_want=()
for app in /Applications/kitty.app "/Applications/Ableton Live 12"*.app "/System/Applications/System Settings.app"; do
[[ -d $app ]] && dock_want+=("file://${app// /%20}/")
done
dock_cur=$(defaults read $D persistent-apps 2>/dev/null | grep -oE '_CFURLString" = "[^"]+' | sed 's/.*= "//' | tr '\n' ' ' || true)
if [[ "$dock_cur" == "${dock_want[*]} " ]]; then log skip "dock apps"; else
backup_domain $D
defaults write $D persistent-apps -array
for u in "${dock_want[@]}"; do
defaults write $D persistent-apps -array-add "<dict><key>tile-data</key><dict><key>file-data</key><dict><key>_CFURLString</key><string>$u</string><key>_CFURLStringType</key><integer>15</integer></dict></dict><key>tile-type</key><string>file-tile</string></dict>"
done
log set "dock apps = ${dock_want[*]}"; CHANGED=1
fi
# ---------- stray network service (best-effort; never fatal) ----------
# macOS refuses -removenetworkservice for the sole service on a PPP port ("parameters were not
# valid"), so this may not remove it. It's a harmless disconnected entry — warn and move on.
if networksetup -listallnetworkservices | grep -qx 'SparkFun Pro Micro'; then
if sudo networksetup -removenetworkservice 'SparkFun Pro Micro' 2>/dev/null; then
log set "removed SparkFun Pro Micro PPP service"; CHANGED=1
else
log warn "could not remove SparkFun PPP service (macOS refuses; harmless — remove via Settings > Network if desired)"
fi
else log skip "no SparkFun service"; fi
# ---------- power (DAW) — AC profile only ----------
pm_cur=$(pmset -g custom | awk '/AC Power/{f=1;next} /Battery Power/{f=0} f && $1 ~ /^(sleep|displaysleep|disksleep|powernap)$/{printf "%s=%s ", $1, $2}')
if [[ $pm_cur == *"sleep=0 "* && $pm_cur == *"displaysleep=30 "* && $pm_cur == *"disksleep=0 "* && $pm_cur == *"powernap=0 "* ]]; then log skip "pmset AC profile"; else
pmset -g custom > "$BK/pmset-$TS.txt"
sudo pmset -c sleep 0 displaysleep 30 disksleep 0 powernap 0; log set "pmset -c sleep 0 displaysleep 30 disksleep 0 powernap 0"; CHANGED=1
fi
setd com.ableton.live NSAppSleepDisabled bool true # App Nap off for Live (domain exists before install; harmless)
# ---------- backup agent ----------
PL="$HOME/Library/LaunchAgents/xyz.sethpc.mac-backup.plist"
if [[ -f $PL ]] && cmp -s "$REPO/config/xyz.sethpc.mac-backup.plist" "$PL"; then log skip "backup launchd agent"; else
mkdir -p "$HOME/Library/LaunchAgents"; cp "$REPO/config/xyz.sethpc.mac-backup.plist" "$PL"
launchctl bootout "gui/$(id -u)/xyz.sethpc.mac-backup" 2>/dev/null || true
# bootstrap into the GUI domain is flaky over SSH (no bound Aqua session); the plist in
# ~/Library/LaunchAgents auto-loads at next login regardless, so never let this abort setup.
if launchctl bootstrap "gui/$(id -u)" "$PL" 2>/dev/null; then log set "backup launchd agent loaded (03:30 nightly)"
else log warn "backup agent copied; bootstrap-over-SSH failed (normal) — loads at next login"; fi
CHANGED=1
fi
# ---------- apply ----------
[[ $CHANGED == 1 ]] && { killall Dock Finder SystemUIServer 2>/dev/null || true; log set "restarted Dock/Finder"; }
# ---------- done ----------
echo; log pubkey "$(cat "$HOME/.ssh/id_ed25519.pub")"
[[ $CHANGED == 1 ]] && log note "some changes (key repeat, scroll direction) apply fully after logout/login"
exit 0
+34
View File
@@ -0,0 +1,34 @@
#!/opt/homebrew/bin/bash
# tank-automount.sh — run ON the Mac as seth. From steel141:
# printf '%s\n' "$HOMELAB_PASSWORD" | ssh mac 'bash ~/mac/scripts/tank-automount.sh'
# tank at /Volumes/tank via autofs + SMB, as Administrator (= root on tank, same as steel141's
# /mnt/Z). autofs mounts on first access and unmounts when idle, so a laptop that leaves the LAN
# gets no login-time "server not found" dialogs and no hung Finder. Password lives ONLY in
# root-owned /etc/auto_smb (0600); it is also seth's sudo password here, so stdin serves both.
# Known limit: automounted fs are always `nobrowse` -> not a drive in Finder's Locations sidebar.
# It's a folder: drag /Volumes/tank to Finder Favorites once (docs/manual-checklist.md).
set -euo pipefail
IFS= read -r PW; [[ -n $PW ]] || { echo "password expected on stdin"; exit 1; }
BK="$HOME/.mac-setup-backup"; TS=$(date +%s); mkdir -p "$BK"
log(){ printf '\033[1;33m[%s]\033[0m %s\n' "$1" "$2"; }
printf '%s\n' "$PW" | sudo -S -p '' -v # one ticket for the run; -K at the end
trap 'sudo -K' EXIT
if grep -qE '^/-[[:space:]]+auto_smb' /etc/auto_master; then log skip "auto_master direct map"; else
sudo cp /etc/auto_master "$BK/auto_master-$TS"
printf '/-\t\t\tauto_smb\t-nosuid\n' | sudo tee -a /etc/auto_master >/dev/null; log set "auto_master += auto_smb"
fi
# password via stdin (never argv) -> %-encoded for the URL
ENC=$(printf '%s' "$PW" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read(),safe=""))')
MAP="/Volumes/tank -fstype=smbfs,soft ://Administrator:${ENC}@192.168.0.173/tank"
# pipeline, not $(sudo cat): the no-tty sudo ticket is per parent pid and a subshell breaks it
if printf '%s\n' "$MAP" | sudo cmp -s - /etc/auto_smb 2>/dev/null; then log skip "/etc/auto_smb"; else
printf '%s\n' "$MAP" | sudo tee /etc/auto_smb >/dev/null # no backup: it's ours and holds the password
sudo chmod 600 /etc/auto_smb; sudo chown root:wheel /etc/auto_smb; log set "/etc/auto_smb"
fi
sudo mkdir -p /Volumes/tank # automount won't create direct-map triggers ("mountpoint unavailable"); a plain dir here persists
sudo automount -vc >/dev/null
if ls /Volumes/tank >/dev/null 2>&1 && mount | grep -q ' /Volumes/tank (smbfs'; then log ok "tank mounted: $(mount | grep ' /Volumes/tank (smbfs' | cut -d' ' -f1)"
else log warn "trigger present but mount failed — off the LAN, or bad SMB credentials? (check: log show --last 2m --predicate 'process == \"automountd\"')"; fi
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# Run on pve173: ssh pve173 "PUB='<pubkey line>' bash -s" < scripts/tank-side.sh
# PUB comes via env (not a command arg) so ssh's remote-side re-splitting can't truncate the
# space-separated key line. $1 kept as a fallback for a local run.
set -euo pipefail
PUB=${PUB:-${1:-}}
[[ $PUB == ssh-ed25519\ AAAA* ]] || { echo "not a full pubkey line: '$PUB'"; exit 1; }
DS=tank/backups/mac; TS=$(date +%s)
if zfs list "$DS" >/dev/null 2>&1; then echo "[skip] dataset $DS"; else zfs create "$DS"; echo "[set] created $DS"; fi
if grep -q "^\[$DS\]" /etc/sanoid/sanoid.conf; then echo "[skip] sanoid stanza"; else
mkdir -p /etc/sanoid/.backup; cp /etc/sanoid/sanoid.conf "/etc/sanoid/.backup/sanoid.conf-$TS"
printf '\n[%s]\n\tuse_template = tank_media\n' "$DS" >> /etc/sanoid/sanoid.conf; echo "[set] sanoid stanza"
fi
KEYLINE="restrict,command=\"/usr/bin/rrsync /$DS\" $PUB"
if grep -qF "$PUB" /root/.ssh/authorized_keys; then echo "[skip] key present"; else
cp /root/.ssh/authorized_keys "/root/.ssh/authorized_keys.bak-$TS"
echo "$KEYLINE" >> /root/.ssh/authorized_keys; echo "[set] rrsync-restricted key added"
fi