diff --git a/CLAUDE.md b/CLAUDE.md index e68a207f..da5df6c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ If you're an AI agent (Claude Code, Codex, etc.) working in this repo, read this | Add a new alias | `config/aliases.sh` (or `aliases_.sh` for env-specific) | | Add a deploy component | Create `deploy_X()` in `deploy.sh` — see [Adding New Features](#adding-new-features) | | Add a custom binary | Drop it in `custom_bins/` (already on PATH); `chmod +x` | +| Install/manage Mac apps | Add a line to `config/apps.conf` → run `app-picker` (gum TUI) → `brew bundle --file=config/Brewfile`. Official casks + `mas` only, **no third-party taps**. Then `scripts/setup/auth-setup` | | Add an encrypted secret | `secrets-edit` (interactive dotenv editor) | | Run an experiment with resource caps | `jexp uv run python -m ...` (Linux: needs pueue + systemd user session) | | Commit / commit + push + PR | `/commit` skill or `/commit-push-sync` | diff --git a/claude/rules/supply-chain-security.md b/claude/rules/supply-chain-security.md index 2378d320..1c552567 100644 --- a/claude/rules/supply-chain-security.md +++ b/claude/rules/supply-chain-security.md @@ -50,6 +50,32 @@ All package managers are configured with a **7-day quarantine** (`min-release-ag - Skip hash verification for production Python dependencies - Bypass min-release-age quarantine without explicit user approval +## GUI Apps & Brewfile (macOS) + +Apps live in `config/apps.conf` (registry) → `config/Brewfile` (generated by `app-picker`). + +- **Homebrew official casks + Mac App Store (`mas`) ONLY.** NEVER add a third-party tap + to `apps.conf`, install.sh, or a Brewfile without explicit user approval. +- **Prefer `mas`** (sandboxed, Apple-reviewed) when an app ships full-featured on the + App Store — highest trust tier ("MAS-first"). Use a cask when MAS is crippled/absent. +- Before adding any app: run `brew info ` / `mas info `, verify vendor + homepage. +- **Never `--no-quarantine`.** Gatekeeper + notarization must stay enabled; that's the + defense against malicious casks (brew also verifies a pinned sha256 on download). +- Tier in `apps.conf`: 1 = official vendor auto-approve · 2 = mature OSS (review) · + 3 = explicit approval (ships `default=false`). + +## curl|bash Installers + +Official-page `curl … | sh` gives **authenticity** (HTTPS proves the domain) but NOT +**integrity** (runs whatever's live, unpinned, unreviewed). Prefer, best→worst: +1. Official Homebrew **formula** if one exists (`uv`, `rustup-init`, `bun`) — vendor's + artifact + sha pin + reviewed PR + reproducible. +2. No formula → `curl -o` a versioned URL and **verify the vendor checksum/signature**. +3. Blind `curl … | sh` only as last resort, HTTPS-to-official-domain only. + +Eyeballing the script ("glance at it") is a smell test for gross tampering, NOT an +integrity control — don't treat it as a safeguard. + ## Secrets Awareness - API keys are scoped per-project via direnv `.envrc`, NOT globally exported diff --git a/config.sh b/config.sh index 20dc5616..fe3307ff 100644 --- a/config.sh +++ b/config.sh @@ -36,9 +36,9 @@ INSTALL_REGISTRY=( "ai-tools|Claude Code, Gemini CLI, Codex CLI|all|true" "extras|hyperfine, gitui, code2prompt, terminal-notifier|all|true" "cleanup|Automatic cleanup (macOS only)|all|true" - "experimental|ty type checker, zerobrew|all|true" + "experimental|ty type checker, zotero MCP|all|true" "macos-settings|macOS system defaults (Dock, Finder, keyboard)|macos|true" - "finicky|Finicky browser routing|macos|true" + "apps|GUI + App Store apps via Brewfile (picker TUI)|macos|true" "docker|Docker engine + compose|linux|true" "pueue|Pueue job scheduler + pueued daemon|linux|true" "create-user|Create non-root dev user|linux|true" @@ -230,7 +230,7 @@ apply_profile() { INSTALL_DOCKER=false INSTALL_EXTRAS=false INSTALL_MACOS_SETTINGS=false - INSTALL_FINICKY=false + INSTALL_APPS=false DEPLOY_EDITOR=false DEPLOY_SERENA=false DEPLOY_GHOSTTY=false diff --git a/config/Brewfile b/config/Brewfile new file mode 100644 index 00000000..a7c7af79 --- /dev/null +++ b/config/Brewfile @@ -0,0 +1,43 @@ +# Brewfile — GENERATED by app-picker from config/apps.conf. Do not edit by hand. +# Regenerate: app-picker (or app-picker --defaults) +# Install: brew bundle --file=config/Brewfile +# Policy: official casks + Mac App Store only; never --no-quarantine. + +# mas-cli drives Mac App Store installs (must be signed into the App Store). +brew "mas" + +# ── Casks (official Homebrew casks) ── +cask "aldente" +cask "alfred" +cask "antigravity" +cask "appcleaner" +cask "beardedspice" +cask "chatgpt" +cask "claude" +cask "cleanshot" +cask "cursor" +cask "dropbox" +cask "finicky" +cask "ghostty" +cask "granola" +cask "keyboardcleantool" +cask "mouseless" +cask "nordvpn" +cask "notion" +cask "popclip" +cask "readdle-spark" +cask "slack" +cask "spotify" +cask "stats" +cask "super-productivity" +cask "tailscale-app" +cask "voiceink" +cask "zed" + +# ── Mac App Store (sandboxed, Apple-reviewed) ── +mas "2FAS Auth Browser Extension", id: 6443941139 +mas "Bear", id: 1091189122 +mas "Bitwarden", id: 1352778147 +mas "Things 3", id: 904280696 +mas "Userscripts", id: 1463298887 +mas "uBlock Origin Lite", id: 6745342698 diff --git a/config/aliases.sh b/config/aliases.sh index 29f8ee91..22e04ad7 100644 --- a/config/aliases.sh +++ b/config/aliases.sh @@ -1322,21 +1322,6 @@ alias ai-update='update-ai-tools' # Detects: brew (macOS), apt/dnf/pacman (Linux) alias pkg-update='update-packages' -# zerobrew: faster Homebrew client (use zb for interactive installs, brew for scripts) -# `zb install` falls back to `brew install` on failure (zerobrew doesn't handle casks) -if command -v zb &>/dev/null; then - zb() { - if [[ "$1" == "install" ]]; then - shift - command zb install "$@" || { echo "→ zb failed, falling back to brew install" >&2; brew install "$@"; } - else - command zb "$@" - fi - } - alias zbi='zb install' - alias zbu='zb uninstall' -fi - # Auto-agent guard controls alias auto-guard='auto-agent-guardctl status' alias auto-approve='auto-agent-guardctl approve' diff --git a/config/apps.conf b/config/apps.conf new file mode 100644 index 00000000..3d29784b --- /dev/null +++ b/config/apps.conf @@ -0,0 +1,115 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# App Registry — single source of truth for GUI apps + App Store apps +# ═══════════════════════════════════════════════════════════════════════════════ +# Consumed by: custom_bins/app-picker (gum toggle TUI → generates config/Brewfile) +# Installed by: brew bundle --file=config/Brewfile (install.sh --apps) +# +# Format (pipe-delimited, one app per line): +# method | id | category | tier | default | name | description | auth +# +# method brew | cask | mas +# id formula name (brew) / cask token (cask) / numeric App Store id (mas) +# category text tasks editor llm cli meetings cloud search messaging +# productivity time voice audio vpn auth safari music misc antivirus security +# tier 1 = official-vendor auto-approve · 2 = mature OSS (review) · 3 = explicit approval +# (tier-3 ships default=false; toggle on deliberately) +# default true | false — initial toggle state in the picker +# name display name; for `mas` this is the exact App Store title in the Brewfile +# auth post-install setup the auth-setup checklist surfaces: +# none login apikey pair-phone safari-ext license alfred things-cloud +# +# SECURITY POLICY (see claude/rules/supply-chain-security.md): +# - Homebrew official casks + Mac App Store only. NEVER add a third-party tap here. +# - Prefer `mas` (sandboxed, Apple-reviewed) when an app ships full-featured on the +# App Store — that's the highest trust tier ("MAS-first"). +# - Run `brew info ` / `mas info ` and verify vendor+homepage before adding. +# - Never use --no-quarantine; Gatekeeper/notarization must stay on. +# ═══════════════════════════════════════════════════════════════════════════════ + +# ─── text ─────────────────────────────────────────────────────────────────── +mas|1091189122|text|2|true|Bear|Markdown notes (App Store only; bearcli symlinked by deploy)|login +cask|notion|text|1|true|Notion|Notes, docs, and wiki|login + +# ─── task management ───────────────────────────────────────────────────────── +mas|904280696|tasks|2|true|Things 3|GTD task manager (App Store only)|things-cloud + +# ─── coding: editors ───────────────────────────────────────────────────────── +cask|cursor|editor|1|true|Cursor|AI-first code editor (config deployed)|login +cask|antigravity|editor|1|true|Antigravity|Google agentic IDE (config deployed)|login +cask|zed|editor|1|true|Zed|Fast collaborative editor (config deployed)|login + +# ─── coding: LLM desktop apps ──────────────────────────────────────────────── +cask|chatgpt|llm|1|true|ChatGPT|OpenAI desktop app|login +cask|claude|llm|1|true|Claude|Anthropic desktop app|login + +# ─── coding: terminal ──────────────────────────────────────────────────────── +cask|ghostty|cli|1|true|Ghostty|GPU-accelerated terminal (config deployed)|none + +# ─── meetings ──────────────────────────────────────────────────────────────── +cask|granola|meetings|2|true|Granola|AI meeting notes|login + +# ─── cloud storage ─────────────────────────────────────────────────────────── +cask|dropbox|cloud|1|true|Dropbox|File sync and storage|login +cask|google-drive|cloud|1|false|Google Drive|Google cloud storage (optional)|login + +# ─── search / launcher ─────────────────────────────────────────────────────── +cask|alfred|search|2|true|Alfred|Launcher + workflows (prefs sync via Dropbox)|alfred + +# ─── messaging ─────────────────────────────────────────────────────────────── +cask|slack|messaging|1|true|Slack|Team messaging|login +cask|readdle-spark|messaging|2|true|Spark|Email client (MAS build also full-featured)|login + +# ─── productivity ──────────────────────────────────────────────────────────── +cask|mouseless|productivity|2|true|Mouseless|Keyboard-driven mouse control (config deployed)|none +cask|popclip|productivity|2|true|PopClip|Text-selection actions (cask not MAS — MAS abandoned)|license + +# ─── time tracking ─────────────────────────────────────────────────────────── +cask|super-productivity|time|2|true|Super Productivity|Task + time tracker|none +brew|wakatime-cli|time|2|false|WakaTime CLI|Coding time tracker (API key via secrets)|apikey + +# ─── voice ─────────────────────────────────────────────────────────────────── +cask|voiceink|voice|2|true|VoiceInk|Local voice-to-text (downloads model on first run)|none + +# ─── audio device management ───────────────────────────────────────────────── +# FineTune: per-app volume + multi-device output/routing + EQ (free OSS SoundSource alt). +# Default OFF: newer + single-maintainer project — adoption is high but bus-factor +# risk remains, so it's a conscious opt-in (see supply-chain-security.md modernity rule). +cask|finetune|audio|2|false|FineTune|Per-app volume + audio device routing + EQ (OSS)|none +# switchaudio-osx: CLI to switch the SYSTEM default input/output device. Core formula, +# OSS, no audio driver. Pairs with an Alfred keyword: SwitchAudioSource -t output -s "AirPods". +brew|switchaudio-osx|audio|2|false|SwitchAudioSource|CLI default input/output device switcher (Alfred-friendly)|none + +# ─── vpn ───────────────────────────────────────────────────────────────────── +cask|nordvpn|vpn|2|true|NordVPN|VPN client (vpn deploy configures split tunnel)|login +cask|tailscale-app|vpn|1|true|Tailscale|Mesh VPN (vpn deploy configures routing)|login + +# ─── auth / passwords / 2FA ────────────────────────────────────────────────── +# Bitwarden via MAS: the Safari extension ships only in the App Store build, which +# also covers the desktop app — so mas covers both. (cask `bitwarden` = desktop only.) +mas|1352778147|auth|1|true|Bitwarden|Password manager (App Store build incl. Safari ext)|login +mas|6443941139|auth|2|true|2FAS Auth Browser Extension|2FA browser extension (pairs with phone)|pair-phone + +# ─── safari extensions (install via mas; ENABLE manually in Safari settings) ── +mas|6745342698|safari|2|true|uBlock Origin Lite|Content blocker (enable in Safari)|safari-ext +mas|1463298887|safari|2|true|Userscripts|Userscript manager (enable in Safari)|safari-ext + +# ─── music ─────────────────────────────────────────────────────────────────── +cask|spotify|music|1|true|Spotify|Music streaming|login + +# ─── misc utilities ────────────────────────────────────────────────────────── +cask|aldente|misc|2|true|AlDente|Battery charge limiter|none +cask|finicky|misc|2|true|Finicky|Browser routing (config deployed)|none +cask|appcleaner|misc|2|true|AppCleaner|Thorough app uninstaller|none +cask|cleanshot|misc|1|true|CleanShot X|Screenshot + screen recording|license +cask|stats|misc|2|true|Stats|Menu-bar system monitor|none +cask|keyboardcleantool|misc|2|true|KeyboardCleanTool|Disable keyboard for cleaning|none +cask|beardedspice|misc|2|true|BeardedSpice|Media keys for web players|none + +# ─── antivirus ─────────────────────────────────────────────────────────────── +# Optional, default OFF. Trellix (university-managed) is NOT here — installed via uni. +# Do NOT run Malwarebytes real-time protection alongside Trellix (on-demand scan is fine). +cask|malwarebytes|antivirus|2|false|Malwarebytes|On-demand malware scanner (optional)|login + +# ─── security: runtime defense ─────────────────────────────────────────────── +# Optional, default OFF (prompts frequently). Objective-See outbound firewall. +cask|lulu|security|2|false|LuLu|Outbound firewall — catches apps phoning home|none diff --git a/config/experimental.yaml b/config/experimental.yaml index cbb2b97d..a1621291 100644 --- a/config/experimental.yaml +++ b/config/experimental.yaml @@ -28,11 +28,3 @@ settings_keys: [] reason: "Rust-based Python type checker, 10-60x faster than mypy/pyright. Alpha — fall back to pyright if gaps block." status: trial - -- name: zerobrew - added: 2026-04-01 - review_by: 2026-07-01 - installed_via: "curl https://zerobrew.rs/install | bash" - settings_keys: [] - reason: "Fast Rust-based Homebrew client. Same UX, faster." - status: trial diff --git a/config/login_items.conf b/config/login_items.conf new file mode 100644 index 00000000..baf22bb7 --- /dev/null +++ b/config/login_items.conf @@ -0,0 +1,31 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# login_items.conf — apps to register as macOS "Open at Login" items +# ═══════════════════════════════════════════════════════════════════════════════ +# Consumed by: scripts/setup/setup-login-items +# +# Why a separate list (not apps.conf): "launch at login" is orthogonal to install. +# apps.conf decides WHICH apps to install; this decides which menu-bar apps should +# auto-start so they're reliably present in the menu bar. +# +# Format (pipe-delimited, one app per line): +# name [| hidden] +# name exact app name in /Applications (without ".app"); must match the +# name macOS shows in System Settings → Login Items +# hidden true | false — start hidden (no window). Default true, which is +# right for menu-bar utilities. Set false for apps you want to open. +# +# ── How customising works (read this — it answers "do I have to edit dotfiles?") ─ +# NO. Day-to-day, just toggle apps in System Settings → General → Login Items. +# setup-login-items is BOOTSTRAP-ONCE + ADDITIVE: +# • it only ADDS a curated app if it's missing AND it hasn't added it before +# • it NEVER removes anything, and NEVER re-adds something you removed manually +# (it records what it added in ~/.config/dotfiles/login-items.bootstrapped) +# • it does NOT run on every deploy — you invoke it (or it's part of app setup) +# This list exists only to SEED a fresh machine's menu bar. Edit it only to change +# what a brand-new install should auto-add. +# ═══════════════════════════════════════════════════════════════════════════════ + +Stats +FineTune +Tailscale +NordVPN diff --git a/config/macos_settings.sh b/config/macos_settings.sh index e27e35eb..21432890 100755 --- a/config/macos_settings.sh +++ b/config/macos_settings.sh @@ -76,6 +76,15 @@ configure_mouse() { configure_dock() { echo " → Configuring Dock..." + # Auto-hide the Dock + defaults write com.apple.dock autohide -bool true 2>/dev/null || true + + # No delay before the Dock appears on hover (instant show) + defaults write com.apple.dock autohide-delay -float 0 2>/dev/null || true + + # Conservative icon size + defaults write com.apple.dock tilesize -int 48 2>/dev/null || true + # Hide recent apps section defaults write com.apple.dock show-recents -bool false 2>/dev/null || true diff --git a/custom_bins/app-picker b/custom_bins/app-picker new file mode 100755 index 00000000..a66d725b --- /dev/null +++ b/custom_bins/app-picker @@ -0,0 +1,135 @@ +#!/usr/bin/env zsh +# ═══════════════════════════════════════════════════════════════════════════════ +# app-picker — browse the app registry, toggle apps, generate a Brewfile +# ═══════════════════════════════════════════════════════════════════════════════ +# Reads: config/apps.conf (method|id|category|tier|default|name|description|auth) +# Writes: config/Brewfile (brew/cask/mas entries for `brew bundle`) +# +# Usage: +# app-picker # gum TUI: space=toggle, enter=confirm → writes Brewfile +# app-picker --defaults # no TUI; select registry defaults, write Brewfile +# app-picker --dry-run # print the Brewfile to stdout, don't write +# app-picker --conf PATH # registry path (default: /config/apps.conf) +# app-picker --file PATH # Brewfile output path (default: /config/Brewfile) +# +# Trust policy: registry is official casks + Mac App Store only (no third-party taps). +# ═══════════════════════════════════════════════════════════════════════════════ +set -euo pipefail + +# Resolve dotfiles dir (this script lives in custom_bins/) +SCRIPT_DIR="${0:A:h}" +DOT_DIR="${DOT_DIR:-${SCRIPT_DIR:h}}" + +CONF="$DOT_DIR/config/apps.conf" +BREWFILE="$DOT_DIR/config/Brewfile" +MODE="tui" # tui | defaults +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --defaults) MODE="defaults"; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --conf) CONF="$2"; shift 2 ;; + --file) BREWFILE="$2"; shift 2 ;; + -h|--help) sed -n '2,18p' "$0"; exit 0 ;; + *) echo "app-picker: unknown arg '$1'" >&2; exit 2 ;; + esac +done + +[[ -f "$CONF" ]] || { echo "app-picker: registry not found: $CONF" >&2; exit 1; } + +# ─── Parse registry ────────────────────────────────────────────────────────── +# Arrays are index-aligned. zsh arrays are 1-based. +typeset -a A_METHOD A_ID A_CAT A_TIER A_DEFAULT A_NAME A_DESC A_AUTH +while IFS='|' read -r method id cat tier def name desc auth; do + [[ -z "${method// }" || "${method[1]}" == "#" ]] && continue + A_METHOD+=("${method// }"); A_ID+=("$id"); A_CAT+=("${cat// }") + A_TIER+=("${tier// }"); A_DEFAULT+=("${def// }") + A_NAME+=("$name"); A_DESC+=("$desc"); A_AUTH+=("${auth// }") +done < "$CONF" + +local n=${#A_METHOD} +(( n > 0 )) || { echo "app-picker: no apps parsed from $CONF" >&2; exit 1; } + +# ─── Build display rows (one per app) + default-selected set ────────────────── +# Row format the TUI shows / we parse back: "\t[cat·Tn] name — desc" +typeset -a rows selected +local i +for (( i=1; i<=n; i++ )); do + local label="${i} [${A_CAT[i]}·T${A_TIER[i]}] ${A_NAME[i]} — ${A_DESC[i]}" + rows+=("$label") + [[ "${A_DEFAULT[i]}" == "true" ]] && selected+=("$label") +done + +# ─── Choose: TUI (gum) or defaults ─────────────────────────────────────────── +# Result is the chosen subset of $rows, newline-separated. +local chosen="" +if [[ "$MODE" == "defaults" ]] || [[ "${NON_INTERACTIVE:-false}" == "true" ]] \ + || ! [[ -t 0 ]] || ! command -v gum &>/dev/null; then + chosen="${(F)selected}" +else + local sel_csv="${(j:,:)selected}" + local height=$(( n + 2 )) + local term_h; term_h=$(tput lines 2>/dev/null || echo 40) + (( height > term_h - 4 )) && height=$(( term_h - 4 )) + chosen=$(gum choose --no-limit --height "$height" \ + --header "Select apps to install (space=toggle, enter=confirm):" \ + --selected "$sel_csv" -- "$rows[@]") || { echo "Cancelled — Brewfile unchanged." >&2; exit 0; } +fi + +# ─── Map chosen rows back to indices ───────────────────────────────────────── +typeset -a pick_idx +local line +while IFS= read -r line; do + [[ -z "$line" ]] && continue + pick_idx+=("${line%% *}") # leading field before the tab = index +done <<< "$chosen" + +(( ${#pick_idx} > 0 )) || { echo "Nothing selected — Brewfile unchanged." >&2; exit 0; } + +# ─── Generate Brewfile ─────────────────────────────────────────────────────── +emit_brewfile() { + local has_mas=false idx + for idx in "${pick_idx[@]}"; do + [[ "${A_METHOD[idx]}" == "mas" ]] && { has_mas=true; break; } + done + + print -r -- "# Brewfile — GENERATED by app-picker from config/apps.conf. Do not edit by hand." + print -r -- "# Regenerate: app-picker (or app-picker --defaults)" + print -r -- "# Install: brew bundle --file=config/Brewfile" + print -r -- "# Policy: official casks + Mac App Store only; never --no-quarantine." + print -r -- "" + + if [[ "$has_mas" == "true" ]]; then + print -r -- "# mas-cli drives Mac App Store installs (must be signed into the App Store)." + print -r -- 'brew "mas"' + print -r -- "" + fi + + # brew formulae + local out + out=$(for idx in "${pick_idx[@]}"; do + if [[ "${A_METHOD[idx]}" == "brew" ]]; then print -r -- "brew \"${A_ID[idx]}\""; fi + done | sort) + [[ -n "$out" ]] && { print -r -- "# ── CLI formulae ──"; print -r -- "$out"; print -r -- ""; } + + # casks + out=$(for idx in "${pick_idx[@]}"; do + if [[ "${A_METHOD[idx]}" == "cask" ]]; then print -r -- "cask \"${A_ID[idx]}\""; fi + done | sort) + [[ -n "$out" ]] && { print -r -- "# ── Casks (official Homebrew casks) ──"; print -r -- "$out"; print -r -- ""; } + + # mas + out=$(for idx in "${pick_idx[@]}"; do + if [[ "${A_METHOD[idx]}" == "mas" ]]; then print -r -- "mas \"${A_NAME[idx]}\", id: ${A_ID[idx]}"; fi + done | sort) + [[ -n "$out" ]] && { print -r -- "# ── Mac App Store (sandboxed, Apple-reviewed) ──"; print -r -- "$out"; } +} + +if [[ "$DRY_RUN" == "true" ]]; then + emit_brewfile +else + emit_brewfile > "$BREWFILE" + echo "✓ Wrote ${#pick_idx} apps to $BREWFILE" + echo " Install with: brew bundle --file=$BREWFILE" +fi diff --git a/install.sh b/install.sh index 817f02a2..cb9fb156 100755 --- a/install.sh +++ b/install.sh @@ -55,7 +55,8 @@ COMPONENTS: --cleanup Enable automatic cleanup (macOS only) --docker Enable Docker installation (Linux only) --pueue Enable Pueue job scheduler (Linux only) - --experimental Enable experimental features (ty type checker, zerobrew) + --experimental Enable experimental features (ty type checker, zotero MCP) + --apps Install GUI + App Store apps via Brewfile picker (macOS) --create-user Create non-root dev user (Linux only) --no- Disable a component (e.g., --no-ai-tools) --force-reinstall Reinstall tools even if present @@ -155,7 +156,13 @@ fi if ! is_installed uv; then log_info "Installing uv..." - curl -LsSf https://astral.sh/uv/install.sh | sh + # Prefer the official Homebrew formula on macOS: sha-pinned + reviewed, unlike + # the upstream curl|bash installer (authentic over HTTPS but not tamper-evident). + if is_macos && cmd_exists brew; then + brew_install uv + else + curl -LsSf https://astral.sh/uv/install.sh | sh + fi fi fi # INSTALL_CORE @@ -195,11 +202,7 @@ if [[ "$INSTALL_EXTRAS" == "true" ]]; then log_section "INSTALLING EXTRAS" # Rust toolchain (needed for code2prompt) - if ! is_installed cargo; then - log_info "Installing Rust..." - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet - fi - source "$HOME/.cargo/env" 2>/dev/null || true + install_rust_toolchain if is_macos; then install_packages brew "${PACKAGES_EXTRAS_MACOS[@]}" @@ -385,12 +388,6 @@ if [[ "$INSTALL_EXPERIMENTAL" == "true" ]]; then uv tool install ty 2>/dev/null || log_warning "ty installation failed" fi - # zerobrew: fast Rust-based Homebrew client (installs to /opt/zerobrew, won't touch /opt/homebrew) - if ! is_installed zb; then - log_info "Installing zerobrew (experimental Homebrew alternative)..." - curl --proto '=https' --tlsv1.2 -fsSL https://zerobrew.rs/install | bash 2>/dev/null || log_warning "zerobrew installation failed" - fi - # zotero-mcp-server: Zotero MCP for citation management (see config/experimental.yaml) if ! is_installed zotero-mcp && cmd_exists uv; then log_info "Installing zotero-mcp-server (citation library management)..." @@ -413,11 +410,36 @@ if [[ "$INSTALL_MACOS_SETTINGS" == "true" ]] && is_macos && [[ -f "$DOT_DIR/conf "$DOT_DIR/config/macos_settings.sh" || log_warning "macOS settings had some errors" fi -# ─── Finicky (macOS) ────────────────────────────────────────────────────────── +# ─── GUI + App Store Apps (macOS, via Brewfile) ─────────────────────────────── +# Pick apps in a TUI (app-picker reads config/apps.conf → generates config/Brewfile), +# then install via `brew bundle`. Official casks + Mac App Store only; never +# --no-quarantine (Gatekeeper/notarization must stay on). Finicky now lives here too. + +if [[ "$INSTALL_APPS" == "true" ]] && is_macos; then + log_section "INSTALLING APPS (Brewfile) 📦" -if [[ "$INSTALL_FINICKY" == "true" ]] && is_macos && ! is_cask_installed finicky; then - log_info "Installing Finicky..." - brew_install finicky true + if ! cmd_exists brew; then + log_warning "Homebrew required for apps — skipping" + else + # gum drives the picker; bootstrap it (tiny formula) if missing. + cmd_exists gum || brew_install gum + + brewfile="$DOT_DIR/config/Brewfile" + if [[ "${NON_INTERACTIVE:-false}" == "true" ]] || ! [[ -t 0 ]]; then + log_info "Non-interactive: using committed Brewfile (run 'app-picker' to customise)" + else + # Interactive: let the user toggle apps, regenerating the Brewfile. + "$DOT_DIR/custom_bins/app-picker" || log_warning "app-picker cancelled — using existing Brewfile" + fi + + if [[ -f "$brewfile" ]]; then + log_info "Installing apps from Brewfile (this can take a while)..." + brew bundle --file="$brewfile" || log_warning "Some Brewfile entries failed (mas needs App Store sign-in)" + log_info "Next: run scripts/setup/auth-setup for logins + signature audit" + else + log_warning "No Brewfile at $brewfile — run 'app-picker' first" + fi + fi fi # ─── Done ───────────────────────────────────────────────────────────────────── diff --git a/iterm/onedark.itermcolors b/iterm/onedark.itermcolors deleted file mode 100644 index f848f653..00000000 --- a/iterm/onedark.itermcolors +++ /dev/null @@ -1,303 +0,0 @@ - - - - - Ansi 0 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4392156862745098 - Green Component - 0.38823529411764707 - Red Component - 0.3607843137254902 - - Ansi 1 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4588235294117647 - Green Component - 0.4235294117647059 - Red Component - 0.8784313725490196 - - Ansi 10 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4745098039215686 - Green Component - 0.7647058823529411 - Red Component - 0.596078431372549 - - Ansi 11 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4 - Green Component - 0.6039215686274509 - Red Component - 0.8196078431372549 - - Ansi 12 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.9372549019607843 - Green Component - 0.6862745098039216 - Red Component - 0.3803921568627451 - - Ansi 13 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.8666666666666667 - Green Component - 0.47058823529411764 - Red Component - 0.7764705882352941 - - Ansi 14 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7607843137254902 - Green Component - 0.7137254901960784 - Red Component - 0.33725490196078434 - - Ansi 15 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.3215686274509804 - Green Component - 0.26666666666666666 - Red Component - 0.24313725490196078 - - Ansi 2 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4745098039215686 - Green Component - 0.7647058823529411 - Red Component - 0.596078431372549 - - Ansi 3 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4823529411764706 - Green Component - 0.7529411764705882 - Red Component - 0.8980392156862745 - - Ansi 4 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.9372549019607843 - Green Component - 0.6862745098039216 - Red Component - 0.3803921568627451 - - Ansi 5 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.8666666666666667 - Green Component - 0.47058823529411764 - Red Component - 0.7764705882352941 - - Ansi 6 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7607843137254902 - Green Component - 0.7137254901960784 - Red Component - 0.33725490196078434 - - Ansi 7 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Ansi 8 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.38823529411764707 - Green Component - 0.3215686274509804 - Red Component - 0.29411764705882354 - - Ansi 9 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.27450980392156865 - Green Component - 0.3137254901960784 - Red Component - 0.7450980392156863 - - Background Color - - Color Space - sRGB - Blue Component - 0.20392156862745098 - Green Component - 0.17254901960784313 - Red Component - 0.1568627450980392 - - Bold Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Cursor Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Cursor Text Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.20392156862745098 - Green Component - 0.17254901960784313 - Red Component - 0.1568627450980392 - - Foreground Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Selected Text Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.20392156862745098 - Green Component - 0.17254901960784313 - Red Component - 0.1568627450980392 - - Selection Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - - diff --git a/iterm/onedarker.itermcolors b/iterm/onedarker.itermcolors deleted file mode 100644 index 0f8aee5a..00000000 --- a/iterm/onedarker.itermcolors +++ /dev/null @@ -1,344 +0,0 @@ - - - - - Ansi 0 Color - - Alpha Component - 1 - Blue Component - 0.43921568989753723 - Color Space - sRGB - Green Component - 0.38823530077934265 - Red Component - 0.36078432202339172 - - Ansi 1 Color - - Alpha Component - 1 - Blue Component - 0.45882353186607361 - Color Space - sRGB - Green Component - 0.42352941632270813 - Red Component - 0.87843137979507446 - - Ansi 10 Color - - Alpha Component - 1 - Blue Component - 0.47450980544090271 - Color Space - sRGB - Green Component - 0.76470589637756348 - Red Component - 0.59607845544815063 - - Ansi 11 Color - - Alpha Component - 1 - Blue Component - 0.40000000596046448 - Color Space - sRGB - Green Component - 0.60392159223556519 - Red Component - 0.81960785388946533 - - Ansi 12 Color - - Alpha Component - 1 - Blue Component - 0.93725490570068359 - Color Space - sRGB - Green Component - 0.68627452850341797 - Red Component - 0.3803921639919281 - - Ansi 13 Color - - Alpha Component - 1 - Blue Component - 0.86666667461395264 - Color Space - sRGB - Green Component - 0.47058823704719543 - Red Component - 0.7764706015586853 - - Ansi 14 Color - - Alpha Component - 1 - Blue Component - 0.7607843279838562 - Color Space - sRGB - Green Component - 0.7137255072593689 - Red Component - 0.33725491166114807 - - Ansi 15 Color - - Alpha Component - 1 - Blue Component - 0.15294118225574493 - Color Space - sRGB - Green Component - 0.13333334028720856 - Red Component - 0.12156862765550613 - - Ansi 2 Color - - Alpha Component - 1 - Blue Component - 0.47450980544090271 - Color Space - sRGB - Green Component - 0.76470589637756348 - Red Component - 0.59607845544815063 - - Ansi 3 Color - - Alpha Component - 1 - Blue Component - 0.48235294222831726 - Color Space - sRGB - Green Component - 0.75294119119644165 - Red Component - 0.89803922176361084 - - Ansi 4 Color - - Alpha Component - 1 - Blue Component - 0.93725490570068359 - Color Space - sRGB - Green Component - 0.68627452850341797 - Red Component - 0.3803921639919281 - - Ansi 5 Color - - Alpha Component - 1 - Blue Component - 0.86666667461395264 - Color Space - sRGB - Green Component - 0.47058823704719543 - Red Component - 0.7764706015586853 - - Ansi 6 Color - - Alpha Component - 1 - Blue Component - 0.7607843279838562 - Color Space - sRGB - Green Component - 0.7137255072593689 - Red Component - 0.33725491166114807 - - Ansi 7 Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Ansi 8 Color - - Alpha Component - 1 - Blue Component - 0.38823530077934265 - Color Space - sRGB - Green Component - 0.32156863808631897 - Red Component - 0.29411765933036804 - - Ansi 9 Color - - Alpha Component - 1 - Blue Component - 0.27450981736183167 - Color Space - sRGB - Green Component - 0.31372550129890442 - Red Component - 0.7450980544090271 - - Background Color - - Alpha Component - 1 - Blue Component - 0.15294118225574493 - Color Space - sRGB - Green Component - 0.13333334028720856 - Red Component - 0.12156862765550613 - - Badge Color - - Alpha Component - 0.5 - Blue Component - 0.0 - Color Space - sRGB - Green Component - 0.1491314172744751 - Red Component - 1 - - Bold Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Cursor Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Cursor Guide Color - - Alpha Component - 0.25 - Blue Component - 1 - Color Space - sRGB - Green Component - 0.9268307089805603 - Red Component - 0.70213186740875244 - - Cursor Text Color - - Alpha Component - 1 - Blue Component - 0.15294118225574493 - Color Space - sRGB - Green Component - 0.13333334028720856 - Red Component - 0.12156862765550613 - - Foreground Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Link Color - - Alpha Component - 1 - Blue Component - 0.73423302173614502 - Color Space - sRGB - Green Component - 0.35916060209274292 - Red Component - 0.0 - - Selected Text Color - - Alpha Component - 1 - Blue Component - 0.20392157137393951 - Color Space - sRGB - Green Component - 0.17254902422428131 - Red Component - 0.15686275064945221 - - Selection Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - - diff --git a/plans/2026-06-16-mac-app-setup-brewfile.md b/plans/2026-06-16-mac-app-setup-brewfile.md new file mode 100644 index 00000000..ec996ef1 --- /dev/null +++ b/plans/2026-06-16-mac-app-setup-brewfile.md @@ -0,0 +1,203 @@ +# Plan: New-Mac App Setup via Brewfile + Toggle TUI + Auth Helpers + +Date: 2026-06-16 +Branch: `claude/quirky-hypatia-wj5t9g` + +## Goal + +Make setting up a fresh Mac a one-command, reviewable experience: +- Install the GUI apps you actually use (casks + Mac App Store). +- Browse each app's description and toggle it on/off before installing. +- Reproducible + re-runnable (Brewfile). +- Encode the ChatGPT trust policy (Homebrew only, official casks, **no new taps**, MAS for App Store vendor apps). +- Prune dotfiles cruft that the security policy says to drop. +- Help with the apps that need auth/manual login. + +Priority order baked into every decision (from your security note): +**Security > Reliability > Reproducibility > Performance > Novelty.** + +--- + +## 1. Architecture + +``` +config/apps.conf # NEW — single source of truth: one line per app + └─ generates → config/Brewfile # brew/cask/mas entries (committed, reproducible) +custom_bins/app-picker # NEW — gum TUI: browse descriptions, toggle, write Brewfile +install.sh --apps # NEW component: bootstrap brew+gum → run picker → brew bundle +scripts/setup/auth-setup # NEW — interactive post-install auth checklist +``` + +Why Brewfile (your choice) + a registry: +- `brew bundle` natively handles `brew "x"`, `cask "x"`, and `mas "App", id: N` — one mechanism covers CLI tools, GUI casks, and App Store apps. +- The committed `config/Brewfile` is the reproducible lock-ish artifact the policy asks for. +- `config/apps.conf` keeps descriptions + category + trust tier + auth notes that a raw Brewfile can't hold; the picker reads it and emits the Brewfile. + +### `config/apps.conf` schema + +``` +# method | id | category | tier | default | name | description | auth +cask | notion | text | 1 | true | Notion | Notes/docs/wiki | login +mas | 904280696 | tasks | 2 | true | Things 3 | GTD task manager (App Store) | things-cloud +brew | wakatime-cli | time | 2 | false | WakaTime CLI | Coding time tracker (API key) | apikey +``` + +- **method**: `brew` (formula) / `cask` / `mas` (App Store). **Selection rule (per your policy: MAS > vendor download > cask):** prefer `mas` when the app is on the App Store AND its sandboxed MAS build isn't feature-crippled (gives sandbox/least-privilege + notarization + no cask supply-chain surface). Use `cask` only when the app needs unsandboxed system access (accessibility, automation, system/network extensions, SMC) or isn't on MAS. Safari extensions are always `mas`. Caveat: `mas` re-installs Apple-ID-owned apps, but first acquisition of paid apps (e.g. Things 3) is a one-time GUI step. +- **tier**: 1 = official vendor auto-approve, 2 = mature OSS review, 3 = needs explicit approval (per your policy). Drives a color tag in the TUI; tier-3 items default OFF. +- **default**: initial toggle state. +- **auth**: token for the auth-setup checklist (`login`, `apikey`, `pair-phone`, `safari-ext`, `license`, `none`). + +### TUI: `gum` (answers your "which needs no install?") + +None of gum/fzf/ratatui preship on a clean Mac, but: +- Homebrew is the *only* hard prereq and `install.sh` already installs it first. +- `gum` is a 1-file brew formula already in your package list and already the engine behind `show_component_menu`. +- ratatui would need a full `cargo build` (heavy, slow on fresh machine). + +So: **bootstrap `gum` immediately after Homebrew**, then reuse the existing menu pattern. The picker shows `name — description` rows grouped by category, with a tier tag, full description visible inline (gum) — space toggles, enter confirms, writes `config/Brewfile`. (If you'd prefer a full-description side panel, fzf `--preview` is a drop-in alternative; gum is the lower-friction default.) + +--- + +## 2. App → install-method mapping (verified) + +Legend: ✅ cask · 🛒 Mac App Store (mas) · ⌘ formula · ⚙️ already has dotfiles *config* (install layer is new) + +| Category | App | Method | Cask/ID | Tier | Notes | +|---|---|---|---|---|---| +| text | Bear | 🛒 | `1091189122` | 2 | App Store only; `bearcli` deploy already symlinks CLI | +| text | Notion | ✅ | `notion` | 1 | | +| tasks | Things 3 | 🛒 | `904280696` | 2 | App Store only | +| coding/editor | Cursor | ✅ | `cursor` | 1 | editor config already deployed | +| coding/editor | Antigravity | ✅ | `antigravity` | 1 | Google; config already deployed | +| coding/editor | Zed | ✅ | `zed` | 1 | config already deployed ⚙️ | +| coding/LLM | ChatGPT | ✅ | `chatgpt` | 1 | OpenAI | +| coding/LLM | Claude | ✅ | `claude` | 1 | Anthropic desktop | +| coding/LLM | Codex CLI | ⌘ | (npm, existing `ai-tools`) | 1 | already installed | +| coding/CLI | Ghostty | ✅ | `ghostty` | 1 | config already deployed ⚙️ | +| meetings | Granola | ✅ | `granola` | 2 | | +| cloud | Dropbox | ✅ | `dropbox` | 1 | | +| cloud | Google Drive | ✅ | `google-drive` | 1 | optional (default OFF) | +| search | Alfred | ✅ | `alfred` | 2 | prefs sync from Dropbox (manual: set sync folder + Powerpack license) | +| messaging | Slack | ✅ | `slack` | 1 | | +| messaging | Spark | 🛒 or ✅ | mas (verify id) / `readdle-spark` cask | 2 | MAS build is full-featured (not crippled) → MAS preferred for sandbox; cask also fine | +| productivity | Mouseless | ✅ | `mouseless` | 2 | config already deployed ⚙️; needs accessibility → cask | +| productivity | PopClip | ✅ | `popclip` | 2 | **Use cask, NOT MAS** — MAS edition abandoned at v2023.9; standalone is sandbox-free + current | +| time | Super Productivity | ✅ | `super-productivity` | 2 | | +| time | WakaTime CLI | ⌘ | `wakatime-cli` | 2 | optional; API key via secrets | +| voice | VoiceInk | ✅ | `voiceink` | 2 | config already deployed ⚙️; downloads model on first run | +| vpn | NordVPN | ✅ | `nordvpn` | 2 | `vpn` deploy already configures split tunnel ⚙️ | +| auth | Bitwarden | ✅ or 🛒 | `bitwarden` cask / mas `1352778147` | 1 | Desktop app **has a cask**. Safari extension ships **only** in the MAS build → use mas if you want the Safari ext (covers both) | +| auth | 2FAS | 🛒 | *verify id* | 2 | mainly phone-paired; Safari ext | +| auth | Tailscale | ✅ | `tailscale-app` | 1 | `vpn` deploy already configures ⚙️ | +| safari-ext | uBlock Origin Lite | 🛒 | `6745342698` | 2 | enable manually in Safari | +| safari-ext | Userscripts | 🛒 | `1463298887` *verify* | 2 | enable manually in Safari | +| safari-ext | 2FAS / Bitwarden | 🛒 | (above) | — | enable manually in Safari | +| music | Spotify | ✅ | `spotify` | 1 | | +| misc | AlDente | ✅ | `aldente` | 2 | | +| misc | Finicky | ✅ | `finicky` | 2 | currently installed inline → fold into Brewfile ⚙️ | +| misc | AppCleaner | ✅ | `appcleaner` | 2 | | +| misc | CleanShot X | ✅ | `cleanshot` | 1 | | +| misc | Stats | ✅ | `stats` | 2 | | +| misc | KeyboardCleanTool | ✅ | `keyboardcleantool` | 2 | | +| misc | BeardedSpice | ✅ | `beardedspice` | 2 | | +| antivirus | Malwarebytes | ✅ | `malwarebytes` | 2 | **optional, default OFF**; lightweight on-demand scanner. **Recommended** AV for personal use | +| antivirus | Trellix | ❌ | — | 3 | Personal install, no cask → checklist manual-install note. **Don't run real-time alongside Malwarebytes.** Recommend skip (heavy enterprise EDR, low value for dev threat model) | + +IDs marked *verify* get a `mas search` / `brew info` check during implementation before committing (policy: `brew info` before install, verify vendor/homepage). Safari extensions can be *installed* but must be *enabled* in Safari manually — the auth checklist will list them. + +### "settings → dotfiles" (menu bar / accessibility / dock) +These aren't apps — they're system defaults. Already handled by `config/macos_settings.sh`. I'll extend it with: +- Dock: which apps are pinned (set from the installed-apps list) + autohide behaviour. +- Menu bar items (where scriptable; Stats/AlDente handle most). +Treated as a follow-up sub-task, not part of the Brewfile. + +--- + +## 3. Auth / manual-setup helper + +New `scripts/setup/auth-setup` (run after `brew bundle`): an interactive gum checklist that, per app needing setup, prints the action and offers to open the app / URL: + +- **git / gh** — already covered (gist sync + `gh auth login`); checklist just verifies. +- **API-key apps (WakaTime)** — wire into existing secrets system (`setup-envrc` / `with-secrets`); no plaintext. +- **GUI logins** (Dropbox, Slack, Spark, Granola, Bitwarden, NordVPN, Tailscale, ChatGPT, Claude, Things Cloud, Spotify) — open app, check off when logged in. Can't be automated (interactive OAuth/passwords) — checklist only. +- **Alfred** — open prefs, point sync folder at Dropbox, apply Powerpack license. +- **Safari extensions** — open Safari → Settings → Extensions, enable uBlock Origin Lite / Bitwarden / 2FAS / Userscripts. +- **VoiceInk** — first-run model download. + +No secrets are stored in plaintext; anything key-based flows through the existing SOPS/BWS path. + +--- + +## 4. Proposed pruning (per-item approval — your choice) + +Driven by your policy ("prefer mature, boring"; "never auto-add taps"; "revisit ZeroBrew in 6–12 months"; "cautious with MCP/random tools"): + +**APPROVED for removal:** + +| # | Remove | Where | Rationale | +|---|---|---|---| +| P1 | **zerobrew** | `install.sh` experimental | Experimental pkg mgr; your note says revisit in 6–12mo. `curl\|bash` install. | +| P3 | **Coven** + `brew tap Crazytieguy/tap` | `install.sh` ai-tools | Third-party tap — violates "never auto-add taps". | + +**KEEP (you declined):** ty type checker, zotero-mcp-server → so the `experimental` component stays. +`OFFICIAL_PLUGINS` audit deferred (can revisit separately). + +--- + +## 5. Policy documentation + +- Extend `claude/rules/supply-chain-security.md` (or add `config/apps.conf` header) with the tier model + "no new taps without approval" + "`brew info` before adding any app". +- Note in `CLAUDE.md` how to add an app (one line in `apps.conf` → re-run picker) and the Brewfile regen flow (`brew bundle dump`-style). +- Optional, not auto-applied: your note prefers `~/code/dotfiles` over `~/.dotfiles`. `DOT_DIR` is auto-detected so nothing breaks either way — I'll mention it but not move the repo. + +--- + +## 6. Malicious apps & executables — install integrity + runtime defense + +Trust tiers gate *what* we install; this section gates *integrity* (is the bytes what the vendor shipped?) and *runtime* (is a trusted-looking app misbehaving?). All additions are official/mature, free unless noted. + +### Already covered +- **macOS**: Gatekeeper + notarization (blocks unsigned/un-notarized on launch), XProtect + XProtect Remediator (Apple malware scanner, auto-updated), App Store sandboxing (MAS apps = highest trust → "MAS-first" rule), TCC permission prompts. +- **Brew casks**: pinned **sha256** verified on download → tampered artifact aborts. +- **Dev deps** (existing): `min-release-age` 7-day quarantine, `ignore-scripts`, weekly `dep-audit`, Socket CLI, gitleaks, pip-audit. + +### Additions (all selected) +1. **Enforce quarantine policy** — never `--no-quarantine` in any cask/Brewfile entry; document that Gatekeeper/notarization must stay enabled. Pure policy, zero cost. → `claude/rules/supply-chain-security.md`. +2. **Signature-verify step in `auth-setup`** — after install, run `spctl --assess --type execute` + `codesign -dv --verbose=4` per app; report any unsigned/un-notarized app before you trust it. Free. +3. **LuLu** (Objective-See, free OSS outbound firewall) — optional cask, **default OFF** (prompts a lot). Fills the runtime/egress gap: catches a signed-but-compromised app phoning home. Document KnockKnock (persistence enumeration) + BlockBlock (persistence alerts) as further optional Objective-See tools. +4. **Harden `curl|bash` installers** — resolves "is official-page curl|bash ok?": + - Official page gives **authenticity** (HTTPS cert proves the domain) but NOT **integrity-over-time** (runs whatever's live, unseen), **pinning** (no agreed sha → tamper passes), or **reproducibility**. + - **Rule, best→worst:** (a) use the official **brew formula** if it exists — `uv`, `rustup-init`, `bun` all do; you get the vendor's artifact + sha pin + reviewed PR + reproducible re-run. (b) No formula → `curl -o` the script to a versioned URL and **verify the vendor's published checksum/signature** if they offer one (this is the actual tamper-evidence). (c) blind `curl … | sh` only as last resort, HTTPS-to-official-domain only. + - **Note on manual inspection:** eyeballing the script ("glance at it") is a low-effort smell test for *gross* tampering (second payloads, surprise `sudo`), NOT an integrity control — a competent attacker defeats it, and you're reading the installer not the binary it fetches. Don't treat it as a safeguard; the safeguards are (a)/(b). + - Migrate existing blind pipes in `install.sh` (uv, rust) to brew formulae / fetch-verify-run. + +--- + +## 7. Files to change + +- **NEW** `config/apps.conf` — app registry (the table above). +- **NEW** `config/Brewfile` — generated, committed. +- **NEW** `custom_bins/app-picker` — gum toggle TUI → writes Brewfile. +- **NEW** `scripts/setup/auth-setup` — auth checklist + `spctl`/`codesign` signature-verify step (§6.2). +- **EDIT** `config.sh` — add `apps` to `INSTALL_REGISTRY`; remove pruned items; add LuLu (optional, OFF) to apps.conf. +- **EDIT** `install.sh` — `--apps` block: bootstrap gum, run picker, `brew bundle --file=config/Brewfile`; remove inline Finicky + pruned experimental/coven blocks; migrate uv/rust `curl|bash` → brew formulae / fetch-verify-run (§6.4). +- **EDIT** `scripts/shared/helpers.sh` — small `brew bundle` + `mas` helpers if needed. +- **EDIT** `claude/rules/supply-chain-security.md`, `CLAUDE.md` — policy + how-to + quarantine/no-`--no-quarantine` rule + curl|bash hardening rule. +- **EDIT** `config/macos_settings.sh` — dock/menu-bar follow-up (optional, can defer). + +## 8. Verification + +- `mas search` / `brew info` each *verify*-flagged id before committing the Brewfile. +- `brew bundle check --file=config/Brewfile` (dry, on a Mac) — can't run here (Linux container); will gate behind a note for you to run, or validate syntax with a parser. +- `app-picker` run with `--dry-run` to confirm it emits a valid Brewfile without installing. +- Shellcheck the new scripts. + +## 9. Resolved decisions + +1. **Prune**: zerobrew + Coven/tap only (P1, P3). ty, zotero-mcp, `experimental` component all stay. +2. **Antivirus**: Trellix = university-managed (checklist note, not Brewfile). Malwarebytes = optional cask, default OFF, conflict note. +3. **Optional apps**: Google Drive + WakaTime default OFF (toggle on in picker). +4. **TUI**: gum (bootstrapped after Homebrew). fzf `--preview` remains a drop-in alt. +5. **Malicious apps/executables** (§6): enforce quarantine policy + `spctl`/`codesign` verify step + LuLu (optional, OFF) + harden `curl|bash` → prefer brew formula, else fetch-verify-run. All four selected. + +All open questions resolved — ready to implement on approval. diff --git a/scripts/setup/auth-setup b/scripts/setup/auth-setup new file mode 100755 index 00000000..a9b22568 --- /dev/null +++ b/scripts/setup/auth-setup @@ -0,0 +1,107 @@ +#!/usr/bin/env zsh +# ═══════════════════════════════════════════════════════════════════════════════ +# auth-setup — post-install login/setup checklist + signature verification +# ═══════════════════════════════════════════════════════════════════════════════ +# Run AFTER `brew bundle`. macOS only. Reads config/apps.conf for per-app auth notes. +# +# Usage: +# auth-setup # interactive checklist (auth steps) + signature verify +# auth-setup --verify # ONLY run the codesign/spctl signature audit +# auth-setup --checklist # ONLY print the auth/login checklist +# +# Nothing here stores secrets. API-key apps are pointed at the existing secrets +# flow (setup-envrc / with-secrets) — never plaintext. +# ═══════════════════════════════════════════════════════════════════════════════ +set -uo pipefail + +SCRIPT_DIR="${0:A:h}" +DOT_DIR="${DOT_DIR:-${SCRIPT_DIR:h:h}}" +CONF="$DOT_DIR/config/apps.conf" + +DO_CHECKLIST=true +DO_VERIFY=true +case "${1:-}" in + --verify) DO_CHECKLIST=false ;; + --checklist) DO_VERIFY=false ;; + -h|--help) sed -n '2,16p' "$0"; exit 0 ;; +esac + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "auth-setup: macOS only (uses spctl/codesign + open)." >&2 + exit 1 +fi +[[ -f "$CONF" ]] || { echo "auth-setup: registry not found: $CONF" >&2; exit 1; } + +hdr() { print -P "%F{cyan}── $* ──%f"; } + +# Human-readable instruction per auth type. +auth_hint() { + case "$1" in + login) echo "Open and sign in." ;; + apikey) echo "API key — add via 'setup-envrc' in the relevant repo (no plaintext)." ;; + pair-phone) echo "Pair with the phone app (scan QR in the desktop/extension)." ;; + safari-ext) echo "Enable in Safari → Settings → Extensions, then grant site access." ;; + license) echo "Enter your license key (paid app)." ;; + alfred) echo "Set sync folder to Dropbox (Prefs → Advanced → Set sync folder), apply Powerpack license." ;; + things-cloud) echo "Sign into Things Cloud to sync across devices." ;; + *) echo "" ;; + esac +} + +# ─── Checklist ─────────────────────────────────────────────────────────────── +print_checklist() { + hdr "Post-install setup checklist" + echo "Apps that need a login or manual step. (GUI logins can't be automated.)" + echo "" + local method id cat tier def name desc auth + while IFS='|' read -r method id cat tier def name desc auth; do + [[ -z "${method// }" || "${method[1]}" == "#" ]] && continue + auth="${auth// }" + [[ -z "$auth" || "$auth" == "none" ]] && continue + printf ' [ ] %-32s %s\n' "$name" "$(auth_hint "$auth")" + done < "$CONF" + + echo "" + hdr "Reminders" + cat <<'EOF' + [ ] App Store — SIGN IN via the App Store GUI first: `mas` can't sign in + (Apple removed the CLI API). A never-acquired free app may + need a one-time "Get" click before `mas install` works. + [ ] git / gh — `gh auth login` (SSH + identity also sync via gist) + [ ] Safari exts — uBlock Origin Lite, Userscripts, Bitwarden, 2FAS: ENABLE in Safari + [ ] Trellix — UNIVERSITY-MANAGED: install/update via your uni, NOT Homebrew. + Do not also run Malwarebytes real-time (on-demand scan is fine). + [ ] Alfred — point sync folder at Dropbox so workflows/prefs follow you + [ ] Login items — run `setup-login-items` to seed menu-bar apps (Stats, FineTune, + Tailscale, NordVPN). Additive/bootstrap-once — won't fight you. +EOF +} + +# ─── Signature / notarization audit (§6.2) ─────────────────────────────────── +# Eyeballing installers is theatre; THIS is the real check — is each app signed +# by a Developer ID and notarized by Apple? +verify_signatures() { + hdr "Signature + notarization audit (/Applications)" + echo "Gatekeeper assessment per installed app. ✓ accepted = signed + notarized." + echo "" + local app appname result + for app in /Applications/*.app; do + [[ -d "$app" ]] || continue + appname="${app:t:r}" + if spctl --assess --type execute "$app" &>/dev/null; then + printf ' %s %s\n' "✓" "$appname" + else + # Not accepted by Gatekeeper — surface the authority so you can judge it. + local authority + authority=$(codesign -dv --verbose=2 "$app" 2>&1 | grep -m1 '^Authority=' | cut -d= -f2-) + printf ' %s %-30s %s\n' "⚠️ " "$appname" "${authority:-unsigned / no Developer ID}" + fi + done + echo "" + echo "⚠️ = not Gatekeeper-accepted: unsigned, ad-hoc, or self-distributed." + echo " Mac App Store + notarized casks should all show ✓. Investigate any ⚠️." +} + +[[ "$DO_CHECKLIST" == "true" ]] && print_checklist +[[ "$DO_CHECKLIST" == "true" && "$DO_VERIFY" == "true" ]] && echo "" +[[ "$DO_VERIFY" == "true" ]] && verify_signatures diff --git a/scripts/setup/setup-login-items b/scripts/setup/setup-login-items new file mode 100755 index 00000000..723f27f5 --- /dev/null +++ b/scripts/setup/setup-login-items @@ -0,0 +1,140 @@ +#!/usr/bin/env zsh +# ═══════════════════════════════════════════════════════════════════════════════ +# setup-login-items — seed macOS "Open at Login" items for menu-bar apps +# ═══════════════════════════════════════════════════════════════════════════════ +# Reads config/login_items.conf and registers each listed app as a login item so +# it's reliably present in the menu bar. macOS only. +# +# DESIGN — bootstrap-once + additive (it will NOT fight your manual changes): +# • Adds a curated app ONLY if it's missing AND we haven't added it before. +# • NEVER removes anything; NEVER re-adds something you removed in System Settings +# (what we've added is recorded in ~/.config/dotfiles/login-items.bootstrapped). +# • Does NOT run on every deploy — invoke it manually (or from the app-setup flow). +# So day-to-day you manage Login Items in System Settings; this only seeds a machine. +# +# Usage: +# setup-login-items # add missing curated apps (respecting prior removals) +# setup-login-items --list # show status of each curated app, change nothing +# setup-login-items --force # re-add even apps you previously removed (ignore state) +# setup-login-items --reset-state # forget what we've bootstrapped (then re-seeds) +# ═══════════════════════════════════════════════════════════════════════════════ +set -uo pipefail +setopt extendedglob # for whitespace-run trimming in the conf-parse loop + +SCRIPT_DIR="${0:A:h}" +DOT_DIR="${DOT_DIR:-${SCRIPT_DIR:h:h}}" +CONF="$DOT_DIR/config/login_items.conf" +STATE="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/login-items.bootstrapped" + +MODE="add" +case "${1:-}" in + --list) MODE="list" ;; + --force) MODE="force" ;; + --reset-state) MODE="reset" ;; + -h|--help) sed -n '2,25p' "$0"; exit 0 ;; + "") ;; + *) echo "setup-login-items: unknown option: $1" >&2; exit 1 ;; +esac + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "setup-login-items: macOS only (uses System Events login items)." >&2 + exit 1 +fi +[[ -f "$CONF" ]] || { echo "setup-login-items: list not found: $CONF" >&2; exit 1; } + +hdr() { print -P "%F{cyan}── $* ──%f"; } + +if [[ "$MODE" == "reset" ]]; then + rm -f "$STATE" + echo "Cleared bootstrap state ($STATE). Next run will re-seed all curated apps." + MODE="add" +fi + +mkdir -p "${STATE:h}" +[[ -f "$STATE" ]] || : > "$STATE" + +# Resolve an app name to its bundle path (/Applications or ~/Applications). +app_path_for() { + local name="$1" + for base in "/Applications" "$HOME/Applications"; do + [[ -d "$base/$name.app" ]] && { echo "$base/$name.app"; return 0; } + done + return 1 +} + +# Current login item names (one per line). +# NB: assumes no login-item display name contains a comma (AppleScript joins the +# list with ", "). True for the curated apps; revisit if that ever changes. +current_login_items() { + osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null \ + | tr ',' '\n' | sed 's/^ *//;s/ *$//' +} + +# Add a login item by path; hidden true|false. +add_login_item() { + local path="$1" hidden="$2" + osascript - "$path" "$hidden" <<'EOF' >/dev/null 2>&1 +on run argv + set appPath to item 1 of argv + set isHidden to (item 2 of argv is "true") + tell application "System Events" + make login item at end with properties {path:appPath, hidden:isHidden} + end tell +end run +EOF +} + +EXISTING="$(current_login_items)" +in_existing() { print -r -- "$EXISTING" | grep -qxF "$1"; } +in_state() { grep -qxF "$1" "$STATE" 2>/dev/null; } + +hdr "Login items (curated: $CONF)" + +added=0 skipped=0 missing=0 +while IFS='|' read -r name hidden; do + name="${name##[[:space:]]##}"; name="${name%%[[:space:]]##}" # strip leading/trailing whitespace runs (incl. tabs) + [[ -z "$name" || "${name[1]}" == "#" ]] && continue + hidden="${hidden// }"; [[ -z "$hidden" ]] && hidden="true" + + apath="$(app_path_for "$name" || true)" + + if [[ "$MODE" == "list" ]]; then + # NB: don't name this 'status' — zsh reserves it (read-only alias for $?). + local st + if [[ -z "$apath" ]]; then st="not installed" + elif in_existing "$name"; then st="login item ✓" + elif in_state "$name"; then st="removed by you (respected)" + else st="installed, NOT a login item" + fi + printf ' %-22s %s\n' "$name" "$st" + continue + fi + + if [[ -z "$apath" ]]; then + printf ' %-22s %s\n' "$name" "skip — not installed" + ((missing++)); continue + fi + if in_existing "$name"; then + in_state "$name" || echo "$name" >> "$STATE" # record so we never re-add + printf ' %-22s %s\n' "$name" "already a login item" + ((skipped++)); continue + fi + if [[ "$MODE" != "force" ]] && in_state "$name"; then + printf ' %-22s %s\n' "$name" "skip — you removed it (use --force to re-add)" + ((skipped++)); continue + fi + + if add_login_item "$apath" "$hidden"; then + in_state "$name" || echo "$name" >> "$STATE" + printf ' %-22s %s\n' "$name" "added (hidden=$hidden)" + ((added++)) + else + printf ' %-22s %s\n' "$name" "FAILED to add" + fi +done < "$CONF" + +[[ "$MODE" == "list" ]] && exit 0 + +echo "" +echo "Added $added · skipped $skipped · not-installed $missing" +echo "Manage these any time in System Settings → General → Login Items — changes there stick." diff --git a/scripts/shared/helpers.sh b/scripts/shared/helpers.sh index 38f38c13..bdab89b5 100644 --- a/scripts/shared/helpers.sh +++ b/scripts/shared/helpers.sh @@ -366,6 +366,30 @@ install_direnv() { fi } +# Rust toolchain (cargo) via rustup. macOS: official Homebrew formula (sha-pinned, +# reviewed) provides `rustup-init`; run it non-interactively. Linux: keep the upstream +# rustup installer but pin TLS (--proto '=https' --tlsv1.2) — no brew dependency. +# See claude/rules/supply-chain-security.md § curl|bash Installers. +install_rust_toolchain() { + if is_installed cargo; then + source "$HOME/.cargo/env" 2>/dev/null || true + return 0 + fi + log_info "Installing Rust toolchain (user-level, no root needed)..." + if is_macos && cmd_exists brew; then + # Official formula ships `rustup-init`; install the default stable toolchain. + brew_install rustup + if cmd_exists rustup-init; then + rustup-init -y --quiet 2>/dev/null || log_warning "rustup-init failed" + elif cmd_exists rustup; then + rustup default stable 2>/dev/null || log_warning "rustup default stable failed" + fi + else + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet + fi + source "$HOME/.cargo/env" 2>/dev/null || true +} + install_bws() { if is_installed bws; then return 0; fi log_info "Installing bws (Bitwarden Secrets Manager CLI)..."