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 configure everything automatically.
After Claude Code finishes, you just need to:
- Grant Hammerspoon Accessibility permissions when macOS prompts you (System Settings > Privacy & Security > Accessibility > enable Hammerspoon)
- Optionally set Hammerspoon to launch at login via its menu bar icon
That's it. Type <instructions> and watch </instructions> appear on the next line, with your cursor positioned between the tags.
5-Minute Manual Method
If you use Claude Code seriously, you've probably noticed that XML-structured prompts get significantly 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 multiple pieces of context in a single message.
The problem: typing closing tags in a terminal is tedious. There's no auto-complete, no Emmet, no IDE shortcuts. You type <instructions>, then you have to manually type </instructions> - and 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 automatically inserts a closing tag on the next line and positions your cursor between them. It works in any app on macOS - Claude Code, iTerm2, VS Code's terminal, wherever you type.
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, just 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 will confirm 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> - Self-closing tags like
<br/>are ignored - no closing tag inserted - Closing tags like
</div>are ignored - no 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 isn't just more readable for you - it's more parseable for Claude. This matters increasingly as prompt complexity grows. Nested context, multiple constraints, examples with expected outputs, persona instructions - all of these benefit from explicit tag boundaries.
Some useful tags to have in your toolkit:
<instructions>- what you want Claude to do<context>- background information<constraints>- rules and boundaries<example>- input/output examples<system-prompt>- persona or behaviour instructions<code>- code snippets for reference<output-format>- 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 tags turns what used to be a minor annoyance into something you don't think about at all.
Tips
- Launch at login: Click the Hammerspoon menu bar icon > Preferences > "Launch Hammerspoon at login."
- Works everywhere: This isn't Claude Code-specific. It works in any text input on macOS - VS Code, Slack, Notes, wherever.
- Auto-reload: The init file includes a path watcher, so any changes you save to
~/.hammerspoon/are picked up automatically without relaunching. - Quick disable: If the auto-close interferes with something (typing HTML in a code editor, for example), press Cmd+Shift+X to toggle it off instantly.
- Customise the toggle: If Cmd+Shift+X conflicts with another shortcut, change the hotkey in
xml_autoclose.luaby editing thehs.hotkey.bindline.
A small quality-of-life fix, but the kind that compounds. Remove the friction, and you'll find yourself reaching for XML tags by default - which is exactly when Claude Code performs at its best.