Teaches how to execute arbitrary Elisp against the running Emacs daemon via emacsclient. Use when you need to trigger Emacs actions from a shell: open files, start captures, run magit, query buffer state, or drive any Emacs function without opening a frame.
This skill teaches you how to send arbitrary Elisp expressions to the user's running Emacs daemon using emacsclient. The daemon is managed by launchd and exposes a Unix socket that emacsclient connects to for evaluation.
When invoked, use the patterns below to construct and run emacsclient commands via the Bash tool. No helper script is involved — the skill is entirely instruction-based.
Use this skill when you need to:
~/.emacs.d/server/server — hardcoded for this user's configurationorg.gnu.emacs.daemonemacsclient (available system-wide via the Nix-managed Emacs install)emacsclient --socket-name="$HOME/.emacs.d/server/server" -e '(your-elisp-here)'
Always use --socket-name with the explicit socket path. Do not rely on the default socket resolution — the user's ec and em wrappers both set this explicitly.
Before sending expressions, verify the daemon is responsive:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "(+ 1 1)" >/dev/null 2>&1
Exit code 0 means the daemon is running and accepting connections. Any non-zero exit means the daemon is unreachable.
If the health check fails, restart via launchctl:
/bin/launchctl kickstart -k gui/$(id -u)/org.gnu.emacs.daemon
sleep 2
The -k flag kills the existing service before starting fresh. The sleep 2 gives the daemon time to initialize and open its socket before the next emacsclient call.
Full pattern — health check then restart if needed:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "(+ 1 1)" >/dev/null 2>&1 || {
/bin/launchctl kickstart -k gui/$(id -u)/org.gnu.emacs.daemon
sleep 2
}
When you need the result of an expression in a shell variable:
result=$(emacsclient --socket-name="$HOME/.emacs.d/server/server" -e '(buffer-name)')
echo "$result"
The return value is printed to stdout as a Lisp-printed form (strings include surrounding quotes).
When you only care that something happens and not what it returns, use -u to suppress output:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -u -e '(org-agenda nil "a")'
Use -n when you do not want to wait for Emacs to finish processing the expression:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(magit-status "/Users/me/nix")'
This returns immediately. Useful when opening interactive UI (magit, agenda) that the user will drive — there is no meaningful return value to wait for.
The expression is passed as a shell argument to emacsclient -e. Quoting rules:
Simple expressions — use single quotes around the Elisp:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e '(find-file "~/notes/inbox.org")'
Expressions containing shell variables — use double quotes and escape the inner Elisp strings:
filepath="/Users/me/nix/bobert/agents/my-agent.md"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "(find-file \"$filepath\")"
Complex expressions with both — assign the Elisp to a variable first to avoid quoting confusion:
filepath="/Users/me/nix/bobert/skills/run-arbitrary-elisp/SKILL.md"
expr="(find-file \"$filepath\")"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "$expr"
Expressions with backticks or special characters — use printf to build the expression:
expr=$(printf '(message "Path is: %s")' "$some_var")
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "$expr"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(find-file "/Users/me/nix/bobert/flake.nix")'
With a shell variable for the path:
filepath="/Users/me/nix/bobert/flake.nix"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e "(find-file \"$filepath\")"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(magit-status "/Users/me/nix")'
For the notes repo:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(magit-status "/Users/me/binwarden/clients")'
The user has named projects in my/projects: "nix", "notes", "clients", "binwarden".
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(my/find-project "nix")'
Available template keys: "l" (log), "e" (event), "t" (todo with deadline), "a" (link), "s" (source block), "b" (budgeting submenu).
# Open capture with the todo template
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(org-capture nil "t")'
# Open capture with the log template
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(org-capture nil "l")'
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(org-agenda nil "a")'
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(org-roam-node-find)'
uuid="FF665E5D-6093-4830-ADB7-48CAE2FA65D0"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e \
"(org-roam-node-visit (org-roam-node-from-id \"$uuid\"))"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(select-frame-set-input-focus (selected-frame))'
If no frame exists yet and you want to open one:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -c -n -e '(select-frame-set-input-focus (selected-frame))'
# Get the current buffer name
result=$(emacsclient --socket-name="$HOME/.emacs.d/server/server" -e '(buffer-name)')
echo "Current buffer: $result"
# Get a variable's value
roam_dir=$(emacsclient --socket-name="$HOME/.emacs.d/server/server" -e 'org-roam-directory')
echo "Roam dir: $roam_dir"
Note: String values are returned wrapped in double quotes (Lisp print format). Strip them with tr -d '"' if you need a bare shell string:
roam_dir=$(emacsclient --socket-name="$HOME/.emacs.d/server/server" -e 'org-roam-directory' | tr -d '"')
emacsclient exits non-zero when:
Recommended pattern for scripts where Emacs availability is optional:
SOCKET="$HOME/.emacs.d/server/server"
if emacsclient --socket-name="$SOCKET" -e "(+ 1 1)" >/dev/null 2>&1; then
emacsclient --socket-name="$SOCKET" -n -e '(find-file "/path/to/file")'
else
echo "Emacs daemon not running — skipping frame open"
fi
Pattern for scripts where Emacs is required (restart if down):
SOCKET="$HOME/.emacs.d/server/server"
emacsclient --socket-name="$SOCKET" -e "(+ 1 1)" >/dev/null 2>&1 || {
/bin/launchctl kickstart -k gui/$(id -u)/org.gnu.emacs.daemon
sleep 2
}
emacsclient --socket-name="$SOCKET" -n -e '(org-agenda nil "a")'
After writing a new file, open it in Emacs so the user sees it immediately:
newfile="/Users/me/nix/bobert/skills/my-skill/SKILL.md"
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e "(find-file \"$newfile\")"
After create_memory writes new .org files, sync the org-roam database:
emacsclient --socket-name="$HOME/.emacs.d/server/server" -u -e '(org-roam-db-sync)'
Hooks run as bash scripts. Use the health-check-then-act pattern to safely drive Emacs from hooks without crashing the hook if Emacs is down:
#!/usr/bin/env bash
SOCKET="$HOME/.emacs.d/server/server"
emacsclient --socket-name="$SOCKET" -e "(+ 1 1)" >/dev/null 2>&1 || exit 0
emacsclient --socket-name="$SOCKET" -n -u -e '(dashboard-refresh-buffer)'
# Check if a specific buffer is open
result=$(emacsclient --socket-name="$HOME/.emacs.d/server/server" \
-e '(if (get-buffer "inbox.org") "open" "closed")' 2>/dev/null)
# result will be "open" or "closed" (with Lisp string quotes)
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "(+ 1 1)"
# Expected output: 2
emacsclient --socket-name="$HOME/.emacs.d/server/server" -n -e '(find-file "/Users/me/nix/CLAUDE.md")'
# Expected: no output, file opens in Emacs
# Stop the daemon first (careful — this kills your session)
# /bin/launchctl kill SIGTERM gui/$(id -u)/org.gnu.emacs.daemon
emacsclient --socket-name="$HOME/.emacs.d/server/server" -e "(+ 1 1)"
# Expected: non-zero exit, error message about socket/connection
/run-arbitrary-elisp
This loads the skill instructions into context. Then use the Bash tool with the patterns above to send Elisp to the running Emacs daemon.
nix run /Users/me/nix/bobert
ls ~/.claude/skills/run-arbitrary-elisp/