Claude Code + Emacs Integration Guide 2026

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Workflow

Set up Claude Code as an integrated development companion within GNU Emacs. Use vterm for interactive sessions, shell-mode for quick queries, and custom elisp functions for context-aware prompting directly from your buffers.

Expected time: 20 minutes Prerequisites: Emacs 29+, vterm package, Claude Code CLI installed, Node.js 18+

Setup

1. Install Required Emacs Packages

;; In your init.el or .emacs
(use-package vterm
  :ensure t
  :config
  (setq vterm-max-scrollback 100000)
  (setq vterm-shell "/bin/zsh"))

(use-package vterm-toggle
  :ensure t
  :bind (("C-c t" . vterm-toggle)
         ("C-c T" . vterm-toggle-cd)))

Vterm provides a full terminal emulator inside Emacs, which Claude Code requires for interactive mode.

2. Create Claude Code Elisp Functions

;; ~/.emacs.d/lisp/claude-code.el

(defgroup claude-code nil
  "Claude Code integration for Emacs."
  :group 'tools)

(defvar claude-code-buffer-name "*claude-code*"
  "Name of the Claude Code vterm buffer.")

(defun claude-code-start ()
  "Start an interactive Claude Code session in vterm."
  (interactive)
  (let ((buf (get-buffer claude-code-buffer-name)))
    (if (and buf (buffer-live-p buf))
        (switch-to-buffer-other-window buf)
      (let ((vterm-shell "claude"))
        (vterm claude-code-buffer-name)))))

(defun claude-code-send-region (start end)
  "Send the selected region to Claude Code with a prompt."
  (interactive "r")
  (let* ((code (buffer-substring-no-properties start end))
         (file-name (buffer-file-name))
         (lang (or (file-name-extension file-name) "text"))
         (prompt (read-string "Prompt: " "Explain this code: "))
         (full-prompt (format "%s\n\n```%s\n%s\n```" prompt lang code)))
    (claude-code--send-to-session full-prompt)))

(defun claude-code-explain-function ()
  "Explain the function at point using Claude Code."
  (interactive)
  (let* ((bounds (bounds-of-thing-at-point 'defun))
         (code (buffer-substring-no-properties (car bounds) (cdr bounds)))
         (prompt (format "Explain this function concisely:\n\n```\n%s\n```" code)))
    (claude-code--send-to-session prompt)))

(defun claude-code-print (prompt)
  "Send a one-shot query to Claude Code and display result."
  (interactive "sPrompt: ")
  (let ((output (shell-command-to-string
                 (format "claude --print %s"
                         (shell-quote-argument prompt)))))
    (with-current-buffer (get-buffer-create "*claude-output*")
      (erase-buffer)
      (insert output)
      (markdown-mode)
      (display-buffer (current-buffer)))))

(defun claude-code--send-to-session (text)
  "Send TEXT to the running Claude Code vterm session."
  (let ((buf (get-buffer claude-code-buffer-name)))
    (unless (and buf (buffer-live-p buf))
      (claude-code-start)
      (setq buf (get-buffer claude-code-buffer-name))
      (sleep-for 2))
    (with-current-buffer buf
      (vterm-send-string text)
      (vterm-send-return))
    (switch-to-buffer-other-window buf)))

(provide 'claude-code)

3. Add Keybindings

;; In init.el
(add-to-list 'load-path "~/.emacs.d/lisp/")
(require 'claude-code)

(global-set-key (kbd "C-c c c") #'claude-code-start)
(global-set-key (kbd "C-c c r") #'claude-code-send-region)
(global-set-key (kbd "C-c c e") #'claude-code-explain-function)
(global-set-key (kbd "C-c c p") #'claude-code-print)

4. Configure Environment

# Ensure your shell exports the API key
echo 'export ANTHROPIC_API_KEY="sk-ant-your-key-here"' >> ~/.zshrc
source ~/.zshrc

5. Verify

# From Emacs: M-x shell-command RET
claude --print "Hello from Emacs"
# Expected output:
# Hello from Emacs! (or similar greeting)

Or press C-c c c to open an interactive Claude Code session in a split window.

Usage Example

Working on a Python project in Emacs with Claude Code:

;; 1. Open your Python file
;; 2. Select a function with C-M-h (mark-defun)
;; 3. Press C-c c r to send the region to Claude Code
;; 4. At the prompt, type: "Add type hints and improve error handling"

Example interaction with a Python function:

# Original function in your buffer
def fetch_user_data(user_id, api_client, cache):
    cached = cache.get(f"user:{user_id}")
    if cached:
        return cached
    response = api_client.get(f"/users/{user_id}")
    if response.status_code == 200:
        data = response.json()
        cache.set(f"user:{user_id}", data, ttl=300)
        return data
    return None

After C-c c r with prompt “Add type hints and improve error handling”:

from typing import Any
from dataclasses import dataclass

@dataclass
class UserData:
    id: str
    name: str
    email: str

class UserFetchError(Exception):
    def __init__(self, user_id: str, status_code: int):
        self.user_id = user_id
        self.status_code = status_code
        super().__init__(f"Failed to fetch user {user_id}: HTTP {status_code}")

def fetch_user_data(
    user_id: str,
    api_client: "HttpClient",
    cache: "CacheBackend",
) -> UserData | None:
    """Fetch user data with caching. Raises UserFetchError on API failure."""
    cache_key = f"user:{user_id}"
    cached: dict[str, Any] | None = cache.get(cache_key)
    if cached is not None:
        return UserData(**cached)

    response = api_client.get(f"/users/{user_id}")

    if response.status_code == 404:
        return None
    if response.status_code != 200:
        raise UserFetchError(user_id, response.status_code)

    data = response.json()
    cache.set(cache_key, data, ttl=300)
    return UserData(**data)

Use C-c c p for quick one-shot queries without entering the interactive session:

C-c c p RET "What's the idiomatic way to handle optional chaining in Emacs Lisp?" RET

Common Issues

Why This Matters

Emacs users who add Claude Code to their workflow report completing complex refactoring tasks 40% faster by eliminating context switches between terminal and editor.