Wiring Claude Code into Emacs: claude-code-ide and agent-shell
Claude Code is as much a part of my studio toolchain now as REAPER — it’s what wired up the reapy MCP bridge in the first place. This post documents how it’s actually plumbed into Emacs, since it turns out there isn’t one integration — there are three, and a handful of gotchas that only showed up once I actually started using it split-screen.
Three separate ways to launch Claude
claude-code-ide.el— a full MCP/IDE integration package (session resume, diagnostics, an emacs-tools MCP server). Wrapped by my ownvxe/claude-code-ide, bound toC-c 9.claude-code.el— a lighter wrapper around the plainclaudeCLI in a terminal buffer. Wrapped byvxe/claude-code. Loaded, but I never got around to binding a key to it.agent-shell— an ACP client that talks toclaude-code-acpinstead of shelling out to the CLI directly. Bound toC-c 0viavxe/agent-shell. No resume prompt, and every call starts a fresh, parallel session landing wherever you’re currently focused (more on that below — it took two attempts to get right).
Three packages that all do roughly the same thing sounds redundant until you remember they arrived at different times solving different problems, and each still does something the others don’t (MCP tool access vs. ACP’s structured permission modes vs. just wanting a dumb terminal).
The C-<tab> collision
agent-shell has its own equivalent of the terminal CLI’s Shift-Tab —
switching between a session’s permission modes (default → accept-edits → plan
→ bypass-permissions, whatever the agent advertises). Its default keymap binds
cycling through them to C-<tab>:
(defvar-keymap agent-shell-mode-map
...
"C-<tab>" #'agent-shell-cycle-session-mode
...)
Which is exactly the key tab-bar-mode already owns globally for tab-next.
Outside an agent-shell buffer, C-<tab> moves between tabs. Inside one, it
silently did something else — cycled the permission mode instead of moving to
the next tab. Easy to not notice until you’re several tabs deep and C-<tab>
stops doing what your hands expect.
Fix: unbind it locally so lookup falls through to the global tab-bar binding
again, and put mode selection on C-c <tab> instead — as a completing-read
picker rather than blind cycling, since I’d rather see the list of modes than
guess how many times to hit a key:
(with-eval-after-load 'agent-shell
(define-key agent-shell-mode-map (kbd "C-<tab>") nil)
(define-key agent-shell-mode-map (kbd "C-c <tab>") #'agent-shell-set-session-mode))
define-key with a nil binding doesn’t just no-op — it clears the entry in
that keymap, so the lookup chain continues past it to the parent keymap and
eventually the global map. Confirmed with a throwaway batch instance before
touching the real config:
C-<tab> now: nil
C-c <tab> and the package’s own C-c C-m (agent-shell-set-session-mode)
are now just two keys for the same picker.
A MOTD so the new binding isn’t forgotten
agent-shell shows a welcome banner on session start — gated by
agent-shell-show-welcome-message (on by default), with the actual text
produced by a per-agent :welcome-function. For Claude Code that’s
agent-shell-anthropic--claude-code-welcome-message. Rather than fork the
function, :filter-return advice appends a line to whatever it returns:
(defun vxe/agent-shell--motd-append (message)
(concat message "\n Pick permission mode: C-c <tab>\n"))
(advice-add 'agent-shell-anthropic--claude-code-welcome-message
:filter-return #'vxe/agent-shell--motd-append)
Every fresh C-c 0 session now prints the reminder along with the usual ASCII
banner.
Parallel sessions, and actually respecting the split
My usual flow is to split the frame — code or notes in one pane, Claude in the
other — and I wanted C-c 0 to just drop a new session into whichever pane
I’m focused in, without disturbing the rest of the layout. First attempt set
agent-shell-display-action to a custom function modeled on stock
display-buffer-same-window, redirecting away from side windows (helm, etc.)
when needed:
(defun vxe/agent-shell--display-buffer (buffer alist)
(unless (cdr (assq 'inhibit-same-window alist))
(let ((win (if (window-parameter (selected-window) 'window-side)
(seq-find (lambda (w) (not (window-parameter w 'window-side)))
(window-list))
(selected-window))))
(when win
(window--display-buffer buffer win 'reuse alist)))))
Looked right in a sandboxed test. Then I actually tried it split-screen and
got a brand new session dropped into a different pane than the one I was
looking at. Turns out agent-shell’s own startup path doesn’t reliably consult
agent-shell-display-action for a session’s initial placement — it can pick
whatever window it wants regardless of that variable, so my fix only ever
covered later redisplays, not the launch itself.
The actual fix uses display-buffer-overriding-action — the one variable
Emacs checks before any package’s own display logic, no exceptions:
(defun vxe/agent-shell--display-in (window)
"Return a `display-buffer' action function that only ever targets WINDOW."
(lambda (buffer alist)
(when (window-live-p window)
(window--display-buffer buffer window 'reuse alist))))
(defun vxe/agent-shell ()
(interactive)
(let* ((target (selected-window))
(display-buffer-overriding-action (list (vxe/agent-shell--display-in target))))
(when (fboundp 'agent-shell-anthropic-start-claude-code)
(agent-shell-anthropic-start-claude-code))))
(global-set-key (kbd "C-c 0") 'vxe/agent-shell)
Capture the window you’re in before the launch, then pin every display attempt made during that launch to it. It doesn’t matter what agent-shell tries to do internally — the override wins, so no other window can be clobbered as a side effect. Confirmed against the actual failure mode in a sandbox before trusting it: a two-pane split with unrelated buffers in each, cursor in the bottom one, launch → the bottom pane gets the new session, the top pane’s buffer is provably untouched.
vxe/agent-shell dropped its old reuse-an-existing-session logic entirely in
the process — every C-c 0 now starts an additional, parallel session (no
prefix arg needed), since agent-shell uniquifies buffer names and collisions
were never actually a problem.
Pasting images into a session
Screenshots come up constantly when debugging anything visual (window
layouts, mixer states, this blog’s own rendering). Turns out agent-shell
already handles it:
C-yjust works — it’s remapped toagent-shell-yank-dwiminagent-shell-mode-map, which checks the system clipboard for an image and attaches it, falling back to normal text yank otherwise.M-x agent-shell-send-clipboard-imagedoes the same explicitly;C-uon it (agent-shell-send-clipboard-image-to) lets you pick which running session to send it to.M-x agent-shell-send-screenshotcaptures directly via macOS’sscreencaptureand inserts it — no clipboard step at all.
The clipboard path depends on an external tool per platform — pngpaste on
macOS, wl-paste/xclip on Linux — detected at runtime via
executable-find. Wasn’t installed here (brew install pngpaste fixed it);
screenshot capture worked out of the box since screencapture ships with
macOS.
Where things ended up
| Key | Command | Package |
|---|---|---|
C-c 9 |
vxe/claude-code-ide |
claude-code-ide.el |
C-c 0 |
vxe/agent-shell (new, parallel, split-aware) |
agent-shell |
C-<tab> / C-S-<tab> |
tab-bar next/previous | stock Emacs |
C-c <tab> / C-c C-m |
agent-shell-set-session-mode (picker) |
agent-shell |
C-c C-c |
agent-shell-interrupt |
agent-shell (stock) |
C-y |
agent-shell-yank-dwim (image-aware) |
agent-shell (stock) |