← Back to all posts
Guide

Auto-Close XML Tags in Your Terminal for Better Claude Code Prompts

1-minute implementation

Copy this into Claude Code and tell it to set this up for you. It will install Hammerspoon, create the auto-close script and write the config.

After Claude Code finishes, you just need to:

  1. Grant Hammerspoon Accessibility permissions when macOS prompts you (System Settings > Privacy & Security > Accessibility > enable Hammerspoon)
  2. You can also set Hammerspoon to launch at login from its menu bar icon

That's it. Type <instructions> and watch </instructions> appear on the next line, with your cursor between the tags.


5-minute manual method

If you use Claude Code seriously, you've probably noticed that XML-structured prompts get much better results. Wrapping your intent in tags like <instructions>, <context>, <example>, and <constraints> helps Claude parse complex prompts more reliably, especially when you're juggling several pieces of context in a single message.

The problem is that typing closing tags in a terminal is tedious. There's no auto-complete, no Emmet and no IDE shortcuts. You type <instructions>, then you type </instructions> by hand. If you're nesting tags or using several in one prompt, the friction adds up. It's a small thing, but it nudges you towards lazier, unstructured prompts.

So I built a keystroke-level automation that fixes this. When you type an opening XML tag and press >, it inserts a closing tag on the next line and puts your cursor between them. It works in any app on macOS, including Claude Code, iTerm2 and VS Code's terminal.

How it works

The solution uses Hammerspoon, a free and open-source macOS automation tool that can intercept keystrokes at the OS level. A Lua script watches what you type, and when it detects you closing an opening XML tag with >, it simulates typing the corresponding closing tag on the next line.

You type:  <instructions>
It becomes:
  <instructions>
  |  <-- cursor here
  </instructions>

Prerequisites

  • macOS (Hammerspoon is macOS-only)
  • Homebrew installed

Step 1: Install Hammerspoon

brew install --cask hammerspoon

Then launch it:

open -a Hammerspoon

macOS will ask you to grant Accessibility permissions. Go to System Settings > Privacy & Security > Accessibility and enable Hammerspoon. You may need to relaunch Hammerspoon after granting the permission.

Step 2: Create the auto-close script

Create the file ~/.hammerspoon/xml_autoclose.lua with the following content:

-- XML Auto-Close Tag
-- Typing <tagname> auto-inserts </tagname> on the next line.
-- Toggle with Cmd+Shift+X.

local M = {}

local buffer = ""
local enabled = true
local isInserting = false
local maxBufferSize = 200

-- Toggle hotkey
hs.hotkey.bind({"cmd", "shift"}, "x", function()
    enabled = not enabled
    hs.alert.show("XML Auto-Close: " .. (enabled and "ON" or "OFF"))
end)

-- Keystroke watcher
M.watcher = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event)
    if not enabled or isInserting then return false end

    local keyCode = event:getKeyCode()
    local flags = event:getFlags()

    -- Ignore keyboard shortcuts
    if flags.cmd or flags.ctrl then return false end

    -- Backspace: trim buffer
    if keyCode == 51 then
        if #buffer > 0 then
            buffer = buffer:sub(1, -2)
        end
        return false
    end

    -- Enter/Return: clear buffer
    if keyCode == 36 then
        buffer = ""
        return false
    end

    local char = event:getCharacters()
    if not char or #char == 0 or #char > 1 then return false end

    buffer = buffer .. char
    if #buffer > maxBufferSize then
        buffer = buffer:sub(-maxBufferSize)
    end

    if char == ">" then
        -- Match <tagname> (hyphens, underscores, colons, dots allowed)
        local tag = buffer:match("<([%a_][%w%-_:.]*)>$")

        -- Also match tags with attributes: <tag attr="val">
        if not tag then
            tag = buffer:match("<([%a_][%w%-_:.]*)%s[^>]*>$")
        end

        -- Ignore self-closing tags like <br/>
        if tag and buffer:match("/>$") then
            tag = nil
        end

        if tag then
            buffer = ""
            isInserting = true

            hs.timer.doAfter(0.03, function()
                -- Insert newline (Shift+Enter for Claude Code)
                hs.eventtap.keyStroke({"shift"}, "return", 0)
                hs.timer.doAfter(0.03, function()
                    hs.eventtap.keyStrokes("</" .. tag .. ">")
                    -- Move cursor between the tags
                    hs.timer.doAfter(0.05, function()
                        hs.eventtap.keyStroke({}, "up", 0)
                        hs.timer.doAfter(0.02, function()
                            hs.eventtap.keyStroke({"cmd"}, "right", 0)
                            hs.timer.doAfter(0.02, function()
                                isInserting = false
                            end)
                        end)
                    end)
                end)
            end)
        end
    end

    return false
end)

M.watcher:start()
return M

Newline key configuration

The script above uses Shift+Enter for newlines, which is a common Claude Code configuration. If your Claude Code uses a different key for newlines, update this line:

hs.eventtap.keyStroke({"shift"}, "return", 0)

For Option+Enter:

hs.eventtap.keyStroke({"alt"}, "return", 0)

You can check your configuration in ~/.claude/keybindings.json. If you haven't customised it, try both and see which one inserts a newline in your Claude Code input.

Step 3: Create the init file

Create ~/.hammerspoon/init.lua:

-- Hammerspoon config
require("xml_autoclose")

-- Auto-reload config on save
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", function(files)
    hs.reload()
end):start()

hs.alert.show("Hammerspoon loaded")

If you already have an init.lua, add require("xml_autoclose") to it.

Step 4: Test it

Open your terminal and type <test>. The moment you press >, you should see </test> appear on the next line, with your cursor between the two tags.

Toggle the feature on and off with Cmd+Shift+X. An alert confirms the current state.

What it handles

  • Simple tags: <instructions> inserts </instructions>
  • Hyphenated tags: <system-prompt> inserts </system-prompt>
  • Namespaced tags: <xml:tag> inserts </xml:tag>
  • Tags with attributes: <div class="foo"> inserts </div>
  • The script ignores self-closing tags like <br/>, so it inserts no closing tag
  • The script also ignores closing tags like </div>, so you never get a double close

Why XML tags matter for Claude Code

Claude models are trained to understand XML-structured input. When you wrap parts of your prompt in semantic tags, Claude gets explicit structural cues about which parts are context, which are instructions, and which are constraints.

Instead of:

Here's some context about my project. It uses React and TypeScript.
Now here's what I want you to do. Refactor the auth module.
Make sure to keep backwards compatibility.

Try:

<context>
My project uses React and TypeScript.
</context>

<instructions>
Refactor the auth module.
</instructions>

<constraints>
Maintain backwards compatibility.
</constraints>

The second version is more readable for you, and Claude parses it more reliably. This matters more as prompts grow complex. Nested context, several constraints, examples with expected outputs and persona instructions all benefit from explicit tag boundaries.

Some useful tags:

  • <instructions> marks what you want Claude to do
  • <context> gives background information
  • <constraints> sets the rules and boundaries
  • <example> holds input and output examples
  • <system-prompt> holds persona or behaviour instructions
  • <code> holds code snippets for reference
  • <output-format> says how you want the response structured

The less friction there is between you and well-structured prompts, the better your Claude Code sessions get. Auto-closing removes a minor annoyance, and you stop thinking about it.

Tips

  • Launch at login. Click the Hammerspoon menu bar icon, then Preferences, then "Launch Hammerspoon at login."
  • Works everywhere. This isn't Claude Code-specific. It works in any text input on macOS, including VS Code, Slack and Notes.
  • Auto-reload. The init file includes a path watcher, so Hammerspoon picks up any change you save in ~/.hammerspoon/ without a relaunch.
  • Quick disable. If the auto-close interferes with something, for example typing HTML in a code editor, press Cmd+Shift+X to toggle it off.
  • Customise the toggle. If Cmd+Shift+X conflicts with another shortcut, change the hotkey in xml_autoclose.lua by editing the hs.hotkey.bind line.

It is a small fix, and the kind that compounds. Once the friction is gone, you reach for XML tags by default, which is when Claude Code performs at its best.