Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agent-vterm

License: GPL v3

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.

Requirements

  • Emacs 27.1+
  • vterm (and its native module)

Install

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 installed

For 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.

use-package (manual / load-path)

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

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))

Elpaca

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-tool agent-vterm-<NAME> commands are generated from agent-vterm-commands. use-package applies :custom after the package loads, so re-running agent-vterm-define-commands in :config ensures commands exist for any tools you added (and is harmless if you didn't). If you only ever use the dispatcher M-x agent-vterm, you can drop this line and the per-tool :bind entries.

Usage

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.

Sending file references

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#L10 for 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.

Configuration

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

agent-vterm-commands

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 in M-x agent-vterm, in the buffer name, and to form the per-tool command agent-vterm-<NAME>.

  • COMMAND — the shell command string sent to the vterm shell on first launch. It may include arguments. If it is nil or the empty string, agent-vterm just opens a shell in the directory and sends nothing.

    A non-empty COMMAND is run with exec, 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 (see agent-vterm-kill-buffer-on-exit) — the shell still sources its rc files first, so PATH and 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 — call agent-vterm-define-commands afterward (it runs once automatically when the package first loads). The dispatcher M-x agent-vterm always reflects the current list without regenerating.


agent-vterm-directory-function

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.


agent-vterm-buffer-name-function

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 DIRECTORY and returns the same name for a tool everywhere, you get a single shared buffer per tool across all projects instead of one-per-project.


agent-vterm-display-action

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))

agent-vterm-kill-buffer-on-exit

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 exits

Even 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.


agent-vterm-min-runtime

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-closing

agent-vterm-startup-timeout

Type: 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 time

agent-vterm-startup-settle

Type: 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

Commands and functions

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.

License

agent-vterm is free software, licensed under the GNU General Public License v3.0 or any later version.

About

Emacs package to launch agentic CLIs (Claude Code, Copilot, aider, codex) in a vterm at your project root.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages