Emacs as an Agentic Coding Environment
After evaluating and using agentic coding tools for the better part of the year, and now finally integrating them into my development workflow, the most salient question inevitably came to mind: how do I make this work in Emacs?
Background: My Emacs Journey
I am an Emacs lifer. Other than Notepad or TextEdit, I can't recall using another editor before it, and it was the editor I wrote my first line of code in. Since picking up Emacs, I've seen numerous editors and IDEs over the years, but none of them stuck with me or interested me as much as Emacs.
I can't place the exact date, but my journey with Emacs started back in the late aughts. Unsurprisingly, my first encounter with it coincided with my first experience using Linux. Back then—I think it was in 2008 or 2009—the netbook craze was in full swing, so I bought an Asus EEE PC on a lark, then quickly discovered how underpowered it was running the preinstalled OEM version of Windows 7. I decided to give Linux a try and installed Xubuntu, which was suitable for my low-spec netbook.
My main machine at the time was a 1st gen Intel Macbook Pro, which I bought in 2006. I was using it primarily for digital audio projects, and though I wasn't yet into programming, my interest in audio technology brought me within close proximity to it. Later, influenced by a fellow audio technology enthusiast, programmer, and friend, I eventually started using SuperCollider, which opened up an entirely new world to me.
After installing Xubuntu on my netbook, my first order of business was to install SuperCollider and continue my audio experiments there. However, SuperCollider, being primarily a MacOS focused project, did not ship their SCIDE integrated development environment to Linux. The only two options were Vim and—you guessed it—Emacs. For better or for worse (some might say worse… and they might have a point 😅), I chose Emacs.
The rest, as they say, is history.
Using Claude Code and Codex CLIs with Vterm
Back to the topic at hand. My first attempt at integrating Claude and ChatGPT found me running Claude Code and Codex (respectively) using vterm, a modern terminal emulator for Emacs. On second thought… allow me one more, brief diversion into Emacs terminals before we get into vterm.
A Brief Technical Note on Emacs Terminals
Proper terminal emulation has long been considered the missing link for Emacs users wanting to do everything from within Emacs, as most of the available options are pretty limited.
For example, the built-in term is written in Emacs Lisp, which runs in a single thread. It creates a shell sub-process that in turn allocates a pseudo terminal for receiving and interpreting shell commands. Emacs handles user input, which it then parses and sends as a string to the sub-process.
From there, an event handler function called a process filter handles output generated by the sub-process. Roughly, the process filter:
- processes each byte, one at a time, transforming each byte into an Emacs text object,
- applies faces for color and styling,
- updates the cursor position, handles scrolling, etc.,
- then renders the output to the buffer.
This is slow when compared to a purpose-built terminal emulator app,
which can take advantage of multiple operating system threads and are
designed to stream large volumes of text quickly. Terminal emulator
apps also implement a much larger subset of terminal escape codes than
term, so they are much better suited to running rich TUI
applications like Claude Code or Codex.
Enter Vterm
Emacs vterm, which is itself based on libvterm, addresses a lot of these issues. In short, it gets around the limitations of Elisp terminals by delegating emulation to an efficient C library, which Emacs loads as a dynamic module. Vterm fully controls parsing of terminal escape codes and maintains the state of the terminal, tracking whether the virtual terminal screen needs to be updated and periodically writing updates in batches to the Emacs buffer.
It's about as smooth of a terminal emulation experience as you're likely to get from within Emacs.
Configuration
To install vterm, I first needed to install a number of dependencies
described in the README From there, adding the dependency via
use-package
is as simple as the following bit of elisp:
(use-package vterm
:ensure t)
From there, it is just a matter of firing up a terminal and running Claude or Codex as one normally would.
Testing
While this generally works well enough, it wasn't long before some
annoyances started to appear. For example, Claude seems to use the
/tui fullscreen setting by default, and while this works great in a
dedicated terminal emulator app, it causes problems in Emacs:
- vterm scrollback doesn't work. while this isn't a problem for native terminals, this doesn't really work with vterm due to Claude Code's alternate screen buffer design.
- claude's scrollback is limited to 2000 lines.
The alternative that I landed on was to just use the /tui default
setting, which at least allows me to scroll back—although, even there,
I need to enable vterm-copy-mode if Claude is in the middle of
writing something, as vterm will automatically scroll to newly written
output. Additionally, to avoid quickly exhausting the scrollback
limit, I needed to update my config accordingly:
(use-package vterm
:ensure t
:init
;; Increase max scrollback so it isn't quickly exhausted
;; for longer sessions.
(setq vterm-max-scrollback 100000))
There are still yet other annoyances, for example redrawing issues causing line heights to shift and seemingly lose focus on prompt inputs, among others. This seems to happen quite often when resizing the window containing the vterm buffer, and it creates an absolute visual mess.
It seemed like there has to be something else out there.
More Bells and Whistles with Claude Code IDE
By the time I arrived at Claude Code IDE
for Emacs, I was using Claude Code for the majority of my agentic
coding "needs", and was hoping to find a solution that addressed many
of the problems I encountered using the vanilla vterm and claude setup I
described earlier. From the README:
Claude Code IDE for Emacs provides native integration with Claude Code CLI through the Model Context Protocol (MCP). […] Transforms Claude into a true Emacs-aware AI assistant that works within your existing workflow and can interact with your entire Emacs ecosystem.
Sounds promising.
Configuration
Installing Claude Code IDE via use-package is also straightforward:
(use-package claude-code-ide
:ensure t
:vc (:url "https://github.com/manzaltu/claude-code-ide.el" :rev :newest)
:bind (("C-c '" . claude-code-ide-menu))
:config
(claude-code-ide-emacs-tools-setup))
Note, since the package is not available via MELPA, I have to
explicitly set the version control host and point it at the author's
Github repository. I've also added a key binding to bring up the IDE
menu, which uses Transient.
Also, in order to enable MCP integration between Emacs and Claude, I
instruct Emacs to evaluate the claude-code-ide-emacs-tools-setup
function after the claude-ide-package loads.
Testing
Claude Code IDE definitely improves upon the vanilla vterm setup I described earlier.
The first thing I noticed after starting a new session interactively
via claude-code-ide, Emacs opens and displays a vterm buffer running
Claude in a side window, and it shows the current file I am visiting,
which is a nice touch.
The automatic layout Claude Code IDE chooses is not ideal on my 14"
laptop, as it occupies most of the Emacs frame. Adjusting via
(enlarge|shrink)-window-horizontally works, but this still seems to
intermittently cause the redrawing issues mentioned above.
Also, since I am used to hiding buffers with delete-window,
delete-other-window etc, which don't work in this setup, I instead
invoke the claude-code-ide-menu function interactively and use the
w key to quickly toggle the side window. This is just a minor
workflow adjustment, and the transient menu is nice enough that this
isn't a major issue for me.
Some of the more interesting bits with this setup is the MCP integration with Emacs, which allows Claude to interact with various Emacs capabilities from within Claude Code. Querying Claude code about those capabilities, it shows the following:
| Tool | Use it for |
|----------------------------|---------------------------------------------|
| `imenu-list-symbols` | Outline of a file: functions, headings |
| `xref-find-references` | Where an identifier is used |
| `xref-find-apropos` | Search symbols by pattern |
| `treesit-info` | Tree-sitter syntax tree at a spot in a file |
| `project-info` | Project root and metadata from project.el |
| `mcp__ide__getDiagnostics` | flycheck/flymake errors and warnings |
For example, running project-info at the prompt (notably, it is not
a slash command) reports various info regarding the project, including
the project root, and the number of files contained within the
project. Neat.
Prompting Claude to run xref for a file within an Elixir project I
am working in doesn't yield much:
| Tool | Query | Result |
|---------------------------|------------------|-------------------------------------------|
| `xref-find-references` | `compare_answer` | No references found |
| `xref-find-apropos` | `batchim` | One hit: korean.ex:125, `def has_batchim?`|
In my tests, Claude cannot detect whether the Emacs language server is
running, so I tell them that eglot is indeed running. Claude suggests
that the MCP tool itself is problematic, so I ran
xref-find-references for the compare_answer function from within
the buffer containing the relevant file, with eglot running, and sure
enough it found many references to it in other project files. It seems
like this particular MCP tool is not quite there yet, unless I am
doing something wrong (which is probable).
That said, I don't think I would use many of these tools via a Claude
query anyway. If I am running xref-find-references or
imenu-list-symbols, I am most certainly doing that from within the
source code buffer as I am bopping around the code.
I do like the idea of using a transient interface to interact with the
Claude process, and it is nice to be able to send a prompt from within
the minibuffer to get around the usual multiline prompt annoyances (I
always forget to use C-j instead of Shift+Return). But a lot of
the problems with vterm and claude I described above still exist here,
and the split between the transient interface and the claude terminal
buffer creates a workflow that feels a bit disjoint.
Overall, though, it is a net improvement over the previous solution. I also understand that, supposedly, the Eat terminal emulator provides a better experience with Claude Code IDE when compared to vterm, particularly with regard to the line-wrapping woes I experienced with the latter. But I have yet to try it out.
Local Coding Agents with Ollama and Aidermacs
While settling into a reasonable workflow with Claude Code IDE, I also wanted to explore local LLMs for agentic coding. Armed with a new Macbook Pro with 24GB of RAM, I figured it was time to give it a try.
The first solution I explored for a local LLM setup was Aider, an agentic coding application written in Python. It supports both cloud-based backends like Claude, as well as local models hosted via Ollama, or apparently any other model that supports an OpenAI compatible API. And, naturally, there is an Emacs project called Aidermacs, which integrates Aider with Emacs.
Configuration
First, I needed to install Aider, per the docs:
python -m pip install aider-install
aider-install
Then, as per usual, I installed aidermacs via use-package. Since I
wanted to use aider with a local Ollama server, I specify a hook
function to tell aider where the Ollama API base URL is located:
(use-package aidermacs
:ensure t
:bind (("C-c a" . aidermacs-transient-menu))
:hook
((aidermacs-before-run-backend . (lambda ()
(setenv "OLLAMA_API_BASE" "http://localhost:11434")))))
Aidermacs implements a simple shell written in Emacs Lisp using Comint mode. While certainly not as fast as vterm, it is not meant to be a terminal emulator and it doesn't need to be. It feels a lot more Emacs-y a result. Aidermacs also exposes a transient interface, similar to Claude Code IDE, which I found myself using a quite a bit when adding files to the agent's context (this becomes important when dealing with local models and context length limitations; more on that below).
Testing
Ultimately, the headaches of dealing with Aidermacs really had little to do with Aidermacs as such, and more to do with working with highly constrained, under-resourced local model. I had to do a lot of tuning so that it was "usable" for agentic coding (which is to say, barely, at least outside of smaller, tightly constrained tasks).
By default, Ollama's context length term
is very small for anything less than 24 GB of GPU addressable RAM;
with my 24 GB of unified memory the context is limited to 4k tokens,
so the model I was working with, qwen3-coder:30b, would keep showing
me an error like this after every prompt:
Empty response received from LLM. Check your provider account?
Tokens: 1.6k sent, 0 received.
Basically the context is exhausted and the model sends back an empty
reply. I was able to resolve this issue specifying the num_ctx
parameter in the .aider.conf.yaml for the project:
model: ollama_chat/qwen3-coder:30b
extra_params:
num_ctx: 32768 # This can be set globally via `OLLAMA_CONTEXT_LENGTH`
Trying again, the fans really started to kick into high gear on my
laptop. I noticed the Aidermacs shell prompt just kept spinning, but
when inspecting the Ollama server via ollama ps, there was no
Qwen3-Coder process running. I checked the activity monitor and it
showed resident memory nearly maxed out. Since I neglected to inspect
the Ollama logs, it wasn't clear what Ollama was up to, whether it was
attempting to read/write swap or something else entirely—but my system
started slowing to a crawl so I was desperately trying to kill the
Ollama server process. Oops.
On a subsequent attempt, I tried a more reasonable value of 16,384 tokens. The system no longer falls over, though the CPU still works very hard. The results are fine, but nothing to get too excited about.
That said, it does seem OK for very simple things. A unit test here, a short shell script there. The other day I had it write a simple waveform plotter, which worked out reasonably well, even though it used Matplotlib instead of the GNU Plot, despite instructing it to use the latter. 🤦
With respect to Aidermacs as such, an interesting workflow
consideration is that Aider tightly integrates with Git and therefore
assumes you want to use Git. If the directory you are working in
doesn't contain a Git repo, it will ask you to create one. It also
automatically commits every change. If a particular commit doesn't
really do what it says it does, which seems to be the norm when
working with local LLMs, you're gonna be typing the /undo command a
lot. While you can disable auto-commits, the Aider documentation
recommends against it.
Given my frustrations with the local LLM output and my mild irritation at Aider's opinionated workflow preferences, I put Aidermacs down for a bit and retreated to Claude's warm embrace. And for this next, albeit brief, phase, I stopped experimenting and decided to use a setup that was a bit more familiar.
Terminal Emacs with Ghostty, Tmux and Claude Code
OK, I let Claude dictate the process in this brief, Emacs GUI-less interregnum. But I went with a reliable, well-worn solution: cracking open a good, proper terminal emulator in Ghostty, dusting off my tmux config, and running Emacs and Claude in separate tmux windows. Hey, it works—and font rendering and terminal colors in Ghostty look pretty dang good. But still, after about a solid week using this setup, I found myself longing for GUI Emacs. Surely, there's got to be something else I can try, in my never-ending quest for Emacs glory…
Agent-shell and a Unified Emacs Experience
After hearing from a fellow Emacs user and their own experience going through a similar journey, they recommended that I look at agent-shell, so I did precisely that.
From the README, Emacs agent-shell is billed as
A native Emacs shell to interact with LLM agents powered by ACP (Agent Client Protocol).
Rather than relying on a vendor-specific TUI, agent-shell uses ACP; ACP is to agents what LSP (Language Server Protocol) is to programming languages. In other words, it solves the problem of each editor having to implement custom integrations for various agents, as long as the client and the API server used to interact with the agent implement the protocol.
Like Aidermacs, agent-shell uses the familiar comint to implement
it's shell functionality, and it also supports a transient interface
for interaction, so we're in familiar Emacs territory.
Configuration
For Claude Code, I needed to install an separate, external
ACP client
to interact with the Claude Code agent. Via npm:
npm install -g @agentclientprotocol/claude-agent-acp
As per usual, I then installed the package via my Emacs init file:
(use-package agent-shell
:ensure t
:config
;; Uses login-based authentication by default
(setq agent-shell-anthropic-authentication
(agent-shell-anthropic-make-authentication :login t)))
Seeing that agent-shell supports multiple different models, I also
wanted to test it with my local LLM setup, which is exposed via the
OpenCode ACP client, which I install via my package manager:
sudo port install opencode
Some additional configuration is required to make it work with Ollama,
by adding the latter as provider to the OpenCode configuration at
~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"edit": "ask",
"bash": "ask",
"webfetch": "allow"
},
"provider": {
"ollama": "qwen3-coder:30b",
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434"
},
"models": {
"qwen3-coder:30b": {
"name": "Qwen3-Coder 30b"
}
}
}
}
A dummy API key also needs to be configured, apparently:
{
"ollama": {
"type": "api",
"key": "ollama"
}
}
With my configuration in place, I am ready to go.
Testing
While I haven't spent too much time with agent-shell, I am already
feeling like this is possibly the best solution for me, at least for
now. Similar to Aidermacs, the comint-based shell and transient
interface gives it a distinctly Emacs feel. Naturally, I spent most of
my time with the Claude ACP client, and it worked wonderfully.
In particular, the shell felt comfortable and interactive enough
without feeling cluttered. Tool prompts are rendered in the shell as
buttons, which can either be clicked (useful for graphical Emacs) or
executed via single key bindings. For example, when the agent writes
code and proposes a change, it presents an option to view the diff. If
I click the v key, I am presented with an easy to read diff that I
can accept or reject. And, like Aidermacs, the output isn't prone to
weird text rendering issues and line height shifts as seen in vterm.
I also had a bit more luck running local agents this time around when
pairing qwen3-coder:30b with the OpenCode provider. While it is
still a far cry from Claude Code, I tuned my Ollama configuration well
enough by this point that it worked OK for smaller coding tasks. To be
clear, this has little to do with agent-shell. And, at least until
either my hardware or the technology improves, I think I will continue
to get better use out of local LLMs when using them as chat
interfaces, e.g. with Ellama,
rather than as coding agents.
Still, like Aidermacs, it's nice to know that I can use both local and
other LLMs with this solution. If I want to spin up a Claude Code or
Codex session alongside a local one, it's as easy as invoking
agent-shell interactively (or the agent-shell-help-menu) and
starting another session with the desired provider. Unlike Aidermacs,
it also doesn't assume I want to use Git for every project.
Conclusion
For a long-time Emacs user such as myself, there are now a lot of options for working with coding agents without leaving the confines of Emacs.
While vterm paired with the Claude Code CLI is a perfectly workable solution, the small annoyances with terminal emulation within Emacs started to grate. Aidermacs seemed like a good alternative, given its comint and transient style interface, but my frustration getting it to work using local LLMs and it's opinionated workflow constraints steered me away.
After a brief return to running Emacs side-by-side with Claude Code via Ghostty and tmux, I found Agent Shell, which, like Aidermacs, can integrate with multiple LLM providers and uses a more Emacs native feel. Unlike Aidermacs, it doesn't impose a Git-centric workflow and the shell itself is a bit more interactive and user friendly.