Open an agentic CLI (Claude Code, GitHub Copilot CLI, aider, codex, …) inside
a vterm buffer with
default-directory set to the current project root.
Each (project, tool) pair gets its own dedicated buffer. Re-running the same
tool toggles that buffer's window instead of spawning a duplicate process.
- Emacs 27.1+
vterm(and its native module)
agent-vterm is a single file. The minimal install is to put agent-vterm.el
on your load-path and:
(require 'agent-vterm) ; vterm must already be installedFor real configs, use one of the use-package recipes below. Each one is
complete — it shows every option agent-vterm exposes, so you can delete the
lines you don't need.
Works with the use-package built into Emacs 29+ (or the MELPA package on
older Emacs). Point :load-path at wherever you cloned this repo, and make
sure vterm is installed by your package manager.
(use-package agent-vterm
:load-path "~/src/agent-vterm" ; directory containing agent-vterm.el
:after vterm ; ensure vterm is loaded first
:bind (("C-c a a" . agent-vterm) ; dispatcher (C-u prompts for dir)
("C-c a c" . agent-vterm-claude) ; per-tool commands…
("C-c a g" . agent-vterm-copilot)
("C-c a i" . agent-vterm-aider)
("C-c a x" . agent-vterm-codex))
:custom
;; The tools and their shell commands.
(agent-vterm-commands
'(("claude" . "claude")
("copilot" . "copilot")
("aider" . "aider --model sonnet")
("codex" . "codex")))
;; Where to launch — default is project root, falling back to file dir.
(agent-vterm-directory-function #'agent-vterm-project-directory)
;; How buffers are named (also controls reuse/toggle).
(agent-vterm-buffer-name-function #'agent-vterm-default-buffer-name)
;; `display-buffer' ACTION — here, a 40%-tall window at the bottom.
(agent-vterm-display-action
'((display-buffer-in-side-window)
(side . bottom)
(window-height . 0.4)))
;; Close buffer + window when the agent process exits (nil keeps it).
(agent-vterm-kill-buffer-on-exit t)
:config
;; Regenerate the per-tool `agent-vterm-NAME' commands to match the
;; (possibly customized) `agent-vterm-commands' above.
(agent-vterm-define-commands))straight.el clones directly from Git. The :straight recipe pulls
agent-vterm from GitHub; vterm is declared as a dependency, but listing it
explicitly makes the intent clear and lets you pin it.
(use-package vterm
:straight t)
(use-package agent-vterm
:straight (agent-vterm :type git :host github :repo "akagr/agent-vterm.el")
:after vterm
:bind (("C-c a a" . agent-vterm)
("C-c a c" . agent-vterm-claude)
("C-c a g" . agent-vterm-copilot)
("C-c a i" . agent-vterm-aider)
("C-c a x" . agent-vterm-codex))
:custom
(agent-vterm-commands
'(("claude" . "claude")
("copilot" . "copilot")
("aider" . "aider --model sonnet")
("codex" . "codex")))
(agent-vterm-directory-function #'agent-vterm-project-directory)
(agent-vterm-buffer-name-function #'agent-vterm-default-buffer-name)
(agent-vterm-display-action
'((display-buffer-in-side-window)
(side . bottom)
(window-height . 0.4)))
(agent-vterm-kill-buffer-on-exit t)
:config
(agent-vterm-define-commands))With Elpaca and its use-package
integration (elpaca-use-package-mode), :ensure takes the same recipe form.
(use-package vterm
:ensure t)
(use-package agent-vterm
:ensure (agent-vterm :host github :repo "akagr/agent-vterm.el")
:after vterm
:bind (("C-c a a" . agent-vterm)
("C-c a c" . agent-vterm-claude)
("C-c a g" . agent-vterm-copilot)
("C-c a i" . agent-vterm-aider)
("C-c a x" . agent-vterm-codex))
:custom
(agent-vterm-commands
'(("claude" . "claude")
("copilot" . "copilot")
("aider" . "aider --model sonnet")
("codex" . "codex")))
(agent-vterm-directory-function #'agent-vterm-project-directory)
(agent-vterm-buffer-name-function #'agent-vterm-default-buffer-name)
(agent-vterm-display-action
'((display-buffer-in-side-window)
(side . bottom)
(window-height . 0.4)))
(agent-vterm-kill-buffer-on-exit t)
:config
(agent-vterm-define-commands))Why the
:config (agent-vterm-define-commands)? The per-toolagent-vterm-<NAME>commands are generated fromagent-vterm-commands.use-packageapplies:customafter the package loads, so re-runningagent-vterm-define-commandsin:configensures commands exist for any tools you added (and is harmless if you didn't). If you only ever use the dispatcherM-x agent-vterm, you can drop this line and the per-tool:bindentries.
| Command | Effect |
|---|---|
M-x agent-vterm |
Pick a tool with completion, launch/toggle in project root. |
M-x agent-vterm-claude |
Launch a specific tool directly (one command per tool). |
C-u M-x agent-vterm |
Prompt for the directory before launching. |
M-x agent-vterm-send-dwim |
Insert the current file as @relative/path, or the active region as @relative/path#L10-20, into a running agent. |
Running a command whose buffer is already visible hides it; running it again brings it back — a quick toggle for your agent pane.
From any file buffer, agent-vterm-send-dwim hands the running agent a
reference to what you are looking at, formatted as an @-path that most agent
CLIs (Claude Code, Copilot, aider) resolve themselves:
- With no active region, it inserts
@path/to/file. - With an active region, it inserts
@path/to/file#L10-20(or@path/to/file#L10for a single line).
The path is relative to the agent buffer's working directory. The text is inserted but not submitted, so you can type a prompt around it before sending. When more than one agent is running in the project, you are asked which buffer to target; with exactly one it is used automatically.
All settings are customizable variables. Set them with setq in your init
file, or interactively via M-x customize-group RET agent-vterm RET.
At a glance:
| Variable | Type | Default |
|---|---|---|
agent-vterm-commands |
alist | claude / copilot / aider / codex |
agent-vterm-directory-function |
function | agent-vterm-project-directory |
agent-vterm-buffer-name-function |
function | agent-vterm-default-buffer-name |
agent-vterm-display-action |
display-buffer action |
nil |
agent-vterm-kill-buffer-on-exit |
boolean | t |
agent-vterm-min-runtime |
number (seconds) | 1.5 |
agent-vterm-startup-timeout |
number (seconds) | 5.0 |
agent-vterm-startup-settle |
number (seconds) | 0.4 |
Type: alist of (NAME . COMMAND) • Default:
(("claude" . "claude") ("copilot" . "copilot") ("aider" . "aider") ("codex" . "codex"))
The set of tools agent-vterm can launch.
-
NAME— a short string label. Used for completion inM-x agent-vterm, in the buffer name, and to form the per-tool commandagent-vterm-<NAME>. -
COMMAND— the shell command string sent to the vterm shell on first launch. It may include arguments. If it isnilor the empty string, agent-vterm just opens a shell in the directory and sends nothing.A non-empty
COMMANDis run withexec, so it replaces the shell rather than running as a child of it. This is what lets the buffer tear down when the agent exits (seeagent-vterm-kill-buffer-on-exit) — the shell still sources its rc files first, soPATHand aliases stay available.
(setq agent-vterm-commands
'(("claude" . "claude")
("claude-resume" . "claude --resume") ; same binary, different flags
("copilot" . "copilot")
("aider" . "aider --model sonnet")
("codex" . "codex")
("shell" . nil))) ; just a project shell
;; Re-run this after changing the list to (re)generate the
;; per-tool `agent-vterm-NAME' commands:
(agent-vterm-define-commands)Note: editing this variable does not automatically create the
agent-vterm-<NAME>commands — callagent-vterm-define-commandsafterward (it runs once automatically when the package first loads). The dispatcherM-x agent-vtermalways reflects the current list without regenerating.
Type: function (no args → directory string) • Default:
agent-vterm-project-directory
Decides which directory a tool launches in. Called with no arguments; must return a directory string. Accepted values:
| Value | Behavior |
|---|---|
agent-vterm-project-directory |
(default) Project root via project.el, falling back to the current buffer's default-directory when not in a project. |
(lambda () default-directory) |
Always the current buffer's directory. |
| any custom function | Return whatever directory string you like (e.g. integrate Projectile). |
;; Always launch in the current file's directory:
(setq agent-vterm-directory-function (lambda () default-directory))
;; Use Projectile's root instead of project.el:
(setq agent-vterm-directory-function
(lambda () (or (projectile-project-root) default-directory)))Regardless of this setting, a prefix argument (C-u M-x agent-vterm) prompts
for the directory interactively, using this function's result as the default.
Type: function (NAME DIRECTORY) → string • Default:
agent-vterm-default-buffer-name
Computes the vterm buffer name. Called with the tool NAME and the resolved
launch DIRECTORY. The returned name also determines buffer reuse/toggle: two
launches that produce the same name share one buffer.
The default produces names like *agent:claude@myproject-1a2b3c4d* (tool
name + last path component of the directory + a short hash of its full path).
The hash suffix keeps distinct directories that share a basename (e.g.
~/work/project and ~/play/project) on separate buffers, so they never
collide on the same agent buffer.
;; Plainer names, e.g. "claude [myproject]":
(setq agent-vterm-buffer-name-function
(lambda (name dir)
(format "%s [%s]" name
(file-name-nondirectory (directory-file-name dir)))))Tip: if your function ignores
DIRECTORYand returns the same name for a tool everywhere, you get a single shared buffer per tool across all projects instead of one-per-project.
Type: display-buffer ACTION (sexp) • Default: nil
The ACTION argument passed to display-buffer when showing a tool buffer.
nil uses Emacs's default window-placement rules. See the display-buffer
docstring / the Displaying Buffers section of the Elisp manual for the full
grammar. Common values:
;; Open agents in a 40%-tall window at the bottom of the frame:
(setq agent-vterm-display-action
'((display-buffer-in-side-window)
(side . bottom)
(window-height . 0.4)))
;; Open in a right-hand side window:
(setq agent-vterm-display-action
'((display-buffer-in-side-window)
(side . right)
(window-width . 0.4)))
;; Reuse the current window (no split):
(setq agent-vterm-display-action '(display-buffer-same-window))Type: boolean • Default: t
What happens when the CLI process exits. Accepted values:
| Value | Behavior |
|---|---|
t |
(default) On a clean, non-instant exit, kill the buffer and close its window(s). The window is preserved if it's the only one in its frame, so the frame never collapses. |
nil |
Keep the finished buffer so you can read the final output. No windows are closed. |
(setq agent-vterm-kill-buffer-on-exit nil) ; keep buffers after the agent exitsEven with t, a buffer is kept when its process exits abnormally (non-zero
status or a signal) or sooner than
agent-vterm-min-runtime, so a tool that fails to
start leaves its error on screen instead of vanishing.
This is honored independently of vterm's own vterm-kill-buffer-on-exit:
agent-vterm takes ownership of teardown for its own buffers and never touches
other vterm buffers.
Type: number (seconds) • Default: 1.5
Guards agent-vterm-kill-buffer-on-exit
against hiding failures. A process that exits sooner than this — or with a
non-zero status — almost never represents a real session: the command was
missing, a wrapper rejected it, or a version manager (asdf, nvm, mise) refused
the directory's pinned runtime. In those cases the buffer is kept even when
agent-vterm-kill-buffer-on-exit is t, so the error stays visible instead of
the buffer disappearing the instant you open it.
A clean exit after at least this long is treated as a normal quit and torn down
as usual. Only consulted when agent-vterm-kill-buffer-on-exit is non-nil.
(setq agent-vterm-min-runtime 3.0) ; insist a session last 3s before auto-closingType: number (seconds) • Default: 5.0
Upper bound on how long to wait for the shell prompt before sending a tool's
command. On a fresh launch, agent-vterm polls the new vterm buffer and sends the
command once the shell has settled at its prompt (see
agent-vterm-startup-settle). If the shell never
settles within this many seconds, the command is sent anyway as a fallback.
Raise it for shells with slow startup (heavy rc files, network-mounted homes);
the value is only an upper bound, so a normal shell sends as soon as its prompt
is idle.
(setq agent-vterm-startup-timeout 10.0) ; allow a slow-starting shell more timeType: number (seconds) • Default: 0.4
How long the new vterm buffer's output must stop growing before agent-vterm
treats the shell as ready and sends the command. Waiting for this quiescence —
rather than firing on the first byte of output — is what keeps startup robust in
directories whose shell hooks print before the prompt is ready: direnv
loading an .envrc, a version manager reacting to .tool-versions/.nvmrc, a
project banner, and so on.
Without it, that early output is mistaken for the prompt, the command is sent
mid-initialization, and the exec fails — taking the buffer (and its window)
down with it. That is the directory-dependent "the agent buffer immediately
disappears" failure. Raise this if a fancy/async prompt (e.g. an instant prompt
that redraws after git status resolves) still races the command; lower it to
launch a beat sooner on simple prompts.
(setq agent-vterm-startup-settle 0.8) ; give an async prompt more room to settle| Name | Kind | Description |
|---|---|---|
agent-vterm |
command | Dispatcher. Reads a tool with completion (or pass one as an argument); C-u prompts for the directory. |
agent-vterm-<NAME> |
command | Generated per entry in agent-vterm-commands (e.g. agent-vterm-claude). Launches that tool directly. |
agent-vterm-send-dwim |
command | Insert the current file as an @-path, or the active region as an @-path with a line range, into a running project agent (no submit). |
agent-vterm-define-commands |
command | (Re)generates the agent-vterm-<NAME> commands from agent-vterm-commands. |
agent-vterm-project-directory |
function | Default value of agent-vterm-directory-function. |
agent-vterm-default-buffer-name |
function | Default value of agent-vterm-buffer-name-function. |
agent-vterm is free software, licensed under the GNU General Public License v3.0 or any later version.