Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ brew "neovim"
# emacs-plus over core emacs: native-comp, tree-sitter, and a real macOS GUI,
# all on by default in this formula -- do not pass args, they disable bottles.
# Config lives at ~/.config/emacs (see: make setup-emacs).
brew "d12frosted/emacs-plus/emacs-plus@30"
brew "d12frosted/emacs-plus/emacs-plus@31"
brew "autoconf"
brew "automake"
brew "libtool"
Expand Down
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,18 @@ Authoritative source for both is the shipped Zed theme JSON (`~/Library/Applicat

## Emacs

Vanilla Emacs 30 (`emacs-plus@30`) at `~/.config/emacs`, source `home/private_dot_config/emacs/`. Runs as a daemon under `brew services`; `e` and `eg` (in `aliases/dev.zsh`) open terminal and GUI frames. `EDITOR` stays `nvim`.
Vanilla Emacs 31 (`emacs-plus@31`) at `~/.config/emacs`, source `home/private_dot_config/emacs/`. Runs as a daemon under `brew services`; `e` and `eg` (in `aliases/dev.zsh`) open terminal and GUI frames. `EDITOR` stays `nvim`.

`31.0.91` is a **pretest** off the `emacs-31` branch, not a release -- the tap's `emacs-plus@32` (`32.0.50`) is master, not a newer stable. The tap is untrusted by default under current Homebrew, so a fresh machine needs `brew trust d12frosted/emacs-plus` before the formula will even load.

Layout: `early-init.el.tmpl` (pre-frame), `init.el`, `lisp/droo-{defaults,ui,completion,git,lang,lsp}.el`, `themes/synthwave84-soft-theme.el.tmpl`, `banner.txt`. Only the two `.tmpl` files interpolate -- colors live solely in the theme.

Four things that are easy to get wrong:
Six things that are easy to get wrong:

- **Upgrading the formula does not change which Emacs runs.** `emacs-plus`'s `bin/emacs` is a 5-line wrapper that execs the first of `/Applications/Emacs.app`, `~/Applications/Emacs.app`, then its own keg. The `brew services` block runs that wrapper, so with a stale `/Applications/Emacs.app` the `@31` service happily launches Emacs 30 -- `emacs --version` reports the old version from the new keg, which reads like a broken build. Replace the app bundle too (`ditto <keg>/Emacs.app /Applications/Emacs.app`), then `brew unlink emacs-plus@<old> && brew link emacs-plus@<new>`.
- **A major-version upgrade invalidates every `.elc`.** `define-minor-mode` expands differently across versions, so packages compiled by the old Emacs fail at runtime with things like `Symbol's value as variable is void: corfu-mode--set-explicitly`. Fix with `package-recompile-all` (not `byte-recompile-directory`, which has no package load-path in `-Q` and fails most files):
`emacs --batch -l ~/.config/emacs/early-init.el --eval '(progn (require (quote package)) (package-initialize) (package-recompile-all))'`
Tree-sitter grammars survive a library bump and do not need rebuilding; `make emacs-grammars` reports them "already available", which is itself a load test.
- **`~/.emacs.d` silently wins.** `startup--xdg-or-homedot` (`startup.el`) returns `~/.emacs.d` whenever that directory merely _exists_ -- it never checks for an `init.el`. If it reappears, the entire XDG config is ignored with no error. `make doctor` fails on this, and `setup-emacs.sh` offers to trash it.
- **Runtime state must stay out of the config tree.** `~/.config/emacs` is chezmoi-managed, so anything Emacs writes there becomes `chezmoi verify` drift. `early-init.el` redirects `package-user-dir`, the eln cache, `custom-file`, and grammars to XDG data/cache/state. This is why no `home/.chezmoiignore` was needed -- adding one would newly activate as chezmoi's real ignore file.
- **The daemon does not inherit mise.** mise activates from `.zshrc`, which `exec-path-from-shell -l` never sources, so mise-managed servers (`ruff`, `rust-analyzer`) are invisible. `droo-defaults.el` adds `~/.local/share/mise/shims` to `exec-path` explicitly.
Expand Down
98 changes: 82 additions & 16 deletions home/private_dot_claude/hooks/executable_block-rm-rf.sh
Original file line number Diff line number Diff line change
@@ -1,23 +1,89 @@
#!/usr/bin/env bash
# Block rm -rf commands and suggest trash instead
# Block recursive-force rm and suggest trash instead.
#
# Flags are attributed to the rm they actually belong to. An earlier version
# matched the bare string "rm" against the whole command, so it fired on any
# word ending in rm -- terraform, confirm, perform, form -- and scanned for
# -r/-f across unrelated commands, blocking things like "rm a.txt && grep -r -f".
set -euo pipefail

command=$(jq -r '.tool_input.command // empty')
[[ -n "$command" ]] || exit 0

# Match rm with both -r and -f flags in any form:
# - rm -rf, rm -fr (combined)
# - rm -r -f, rm -f -r (separate)
# - rm --recursive --force, rm --force --recursive (long form)
# - xargs rm -rf (piped)
if [[ "$command" =~ rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f ]] ||
[[ "$command" =~ rm[[:space:]]+-[^[:space:]]*f[^[:space:]]*r ]] ||
[[ "$command" =~ rm[[:space:]].*-r.*-f ]] ||
[[ "$command" =~ rm[[:space:]].*-f.*-r ]] ||
[[ "$command" =~ rm[[:space:]].*--recursive.*--force ]] ||
[[ "$command" =~ rm[[:space:]].*--force.*--recursive ]] ||
[[ "$command" =~ xargs[[:space:]].*rm ]]; then
echo "Blocked: rm -rf is destructive. Use 'trash' instead (brew install macos-trash)." >&2
exit 2
fi
# Words that may precede the real command without changing what it is.
is_wrapper() {
case "$1" in
sudo | doas | env | time | command | builtin | exec) return 0 ;;
nohup | nice | ionice | stdbuf) return 0 ;;
*=*) return 0 ;; # VAR=value prefix
*) return 1 ;;
esac
}

# True when the segment runs rm with both a recursive and a force flag, or
# reaches rm through xargs -- there the file list is generated elsewhere, so the
# blast radius is not visible here.
segment_is_dangerous() {
local seg="$1"
local -a tok=()
local i=0 n via_xargs=0 has_r=0 has_f=0 arg

# Quotes are dropped so `bash -c "rm -rf /"` is inspected, not waved through.
seg="${seg//\"/}"
seg="${seg//\'/}"

read -r -a tok <<<"$seg" || true
n=${#tok[@]}

while [[ $i -lt $n ]]; do
if is_wrapper "${tok[i]}"; then
i=$((i + 1))
elif [[ ${tok[i]} == xargs ]]; then
via_xargs=1
i=$((i + 1))
while [[ $i -lt $n && ${tok[i]} == -* ]]; do i=$((i + 1)); done
elif [[ ${tok[i]} =~ ^(bash|sh|zsh|dash|ksh)$ ]]; then
i=$((i + 1))
if [[ $i -lt $n && ${tok[i]} == -c ]]; then i=$((i + 1)); fi
else
break
fi
done

[[ $i -lt $n ]] || return 1
# Strip any leading path so /bin/rm is still rm.
[[ ${tok[i]##*/} == rm ]] || return 1
i=$((i + 1))

[[ $via_xargs -eq 1 ]] && return 0

for ((; i < n; i++)); do
arg="${tok[i]}"
# Everything after -- is a filename, even if it is shaped like a flag.
[[ $arg == -- ]] && break
case "$arg" in
--recursive) has_r=1 ;;
--force) has_f=1 ;;
--*) ;;
-*)
if [[ $arg == *[rR]* ]]; then has_r=1; fi
if [[ $arg == *f* ]]; then has_f=1; fi
;;
esac
done

[[ $has_r -eq 1 && $has_f -eq 1 ]]
}

# Split on shell separators so each segment holds at most one command.
separators=';&|(){}`'
while IFS= read -r segment; do
[[ -n "${segment//[[:space:]]/}" ]] || continue
if segment_is_dangerous "$segment"; then
printf 'Blocked: %s\n' "${segment#"${segment%%[![:space:]]*}"}" >&2
printf "rm -rf is destructive. Use 'trash' instead (brew install macos-trash).\n" >&2
exit 2
fi
done < <(printf '%s\n' "$command" | tr "$separators" '\n')

exit 0
2 changes: 1 addition & 1 deletion home/private_dot_config/emacs/init.el
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

;;; Commentary:

;; Vanilla Emacs 30 with package.el and the built-in use-package. Runtime state
;; Vanilla Emacs 31 with package.el and the built-in use-package. Runtime state
;; lives under XDG directories (see early-init.el); this tree stays read-only as
;; far as Emacs is concerned.
;;
Expand Down
4 changes: 3 additions & 1 deletion home/private_dot_config/emacs/lisp/droo-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ Monaspace installed still gets a usable Emacs rather than a broken one."

(use-package dashboard
:init
(setq dashboard-banner-logo-title "GNU Emacs 30 -- Synthwave84 Soft"
;; Derived, not hardcoded -- the literal drifted behind the installed Emacs.
(setq dashboard-banner-logo-title
(format "GNU Emacs %d -- Synthwave84 Soft" emacs-major-version)
dashboard-startup-banner (expand-file-name "banner.txt" user-emacs-directory)
dashboard-center-content t
dashboard-vertically-center-content t
Expand Down
2 changes: 1 addition & 1 deletion scripts/setup/setup-emacs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
EMACS_CONFIG_DIR="$HOME/.config/emacs"
EMACS_DOTDIR="$HOME/.emacs.d"
BREW_FORMULA="emacs-plus@30"
BREW_FORMULA="emacs-plus@31"

# Source centralized logging
# shellcheck source=../utils/logging.sh
Expand Down