The AppleScript at python/iterm_tab_names.applescript is good for shell-level use. To call it from Elixir code or expose it as an MCP tool that an AI agent can invoke, there is now an Elixir module wrapping it: ItermTabs. This article documents the module, the permission keys, and how to use it from IEx and from MCP.
What it does
ItermTabs is a thin functional module that shells out to the bundled AppleScript via osascript. Two operations:
-
ItermTabs.list/0– return every iTerm2 tab as a list of%{window, tab, name, profile}maps. -
ItermTabs.send_text/2– send text into the first tab whose session or profile name contains the needle string. Returns the buffer diff captured after the command settles.
The AppleScript handles the heavy lifting: matching the needle against session and profile names with a substring test, selecting the matched tab, doing the two-step send (write text ... newline no followed by a real Return keystroke via System Events), and polling iTerm2’s shell-integration property to know when the command finished.
IEx use
Once the module compiles into the running BEAM, it is available in any IEx session attached to the dev node.
iex> ItermTabs.list()
{:ok, [
%{window: 1, tab: 1, name: "⠐ Claude Code (beam.smp)", profile: "DEV"},
%{window: 1, tab: 2, name: "-zsh", profile: "DEV"},
%{window: 1, tab: 3, name: "DEV (-zsh)", profile: "DEV"},
%{window: 2, tab: 1, name: "ssh", profile: "Default"},
...
]}
iex> ItermTabs.send_text("DEV", "mix test")
{:ok, "-- sent 'mix test' to 'DEV (-zsh)'\n--\n...test output..."}
iex> ItermTabs.send_to_claude("/review")
{:ok, "-- sent '/review' to 'Claude Code (beam.smp)'\n--\n..."}
send_to_claude/1 is a convenience alias for send_text("Claude Code", ...).
Failure modes are tagged tuples:
-
{:error, {:script_not_found, path}}– the AppleScript file is not at the expected location relative to the current working directory. -
{:error, {:exit, code, output}}–osascriptreturned a non-zero exit code. Theoutputfield contains the captured stderr.
On Linux (the production host) osascript does not exist, so calls return {:error, {:exit, ...}}. The module is only useful on macOS dev machines.
MCP use
Two tools are exposed:
| Tool | Permission | Description |
|---|---|---|
iterm_tabs_list |
iterm_tabs.list |
List every tab |
iterm_tabs_send |
iterm_tabs.send |
Send text to a matched tab |
An agent calling iterm_tabs_list with the appropriate token gets back JSON:
{
"tabs": [
{"window": 1, "tab": 1, "name": "Claude Code", "profile": "DEV"},
{"window": 1, "tab": 2, "name": "-zsh", "profile": "DEV"}
]
}
iterm_tabs_send takes two required arguments:
{
"needle": "DEV",
"text": "mix test"
}
and returns the captured output:
{
"needle": "DEV",
"text": "mix test",
"output": "-- sent 'mix test' to 'DEV (-zsh)'\n--\n..."
}
Permission keys
Two keys are registered in Permissions.Bootstrap under the iterm_tabs module:
-
iterm_tabs.list(category:read) – list operations -
iterm_tabs.send(category:write) – send-text operation
Auto-roles bundle them as iterm_tabs.read (list only) and iterm_tabs.write (send only). The admin role gets both keys via the existing sync_admin_role pipeline that merges per-tool keys.
Grant carefully. iterm_tabs.send lets the caller execute anything the user could type at a shell. Treat it the same way you would treat granting a remote shell into the developer’s laptop.
How it talks to the AppleScript
The Elixir module uses System.cmd("osascript", [script_path | args], stderr_to_stdout: true). The script path is resolved as Path.join(File.cwd!(), "python/iterm_tab_names.applescript"). Arguments are passed verbatim:
-
list/0callsosascript python/iterm_tab_names.applescript --listand parses the[wN tM] name=... | profile=...lines into maps. -
send_text/2callsosascript python/iterm_tab_names.applescript needle text.
The two-step send (newline-suppressed typing plus a real Return keystroke) lives entirely in the AppleScript – the Elixir module is transport only.
macOS permission requirements
The Return-key step uses tell application "System Events" to keystroke return. That call requires Accessibility permission for the process invoking osascript. When you run from a local IEx, the BEAM (or whatever Terminal session launched it) needs to be granted Accessibility in System Settings -> Privacy & Security -> Accessibility. When the MCP tool fires from the running dev server, the same rule applies to whatever shell launched the server.
The failure mode is (-1719) not allowed to send keystrokes. If you see that, grant Accessibility and re-run.
File layout
The integration touches a small number of files:
-
lib/iterm_tabs.ex– the functional module. IEx surface lives here. -
lib/permissions/iterm_tabs.ex–Permissions.ItermTabswrapper following the standard AccessControlled pattern. Gates each operation against the appropriate key. -
lib/mcp_server/tools/iterm_tabs.ex–MCPServer.Tools.ItermTabshandler. Translates MCP calls into permission-wrapped Elixir calls. -
lib/permissions/bootstrap.ex– registers the two permission keys. -
lib/mcp_server.ex– adds theiterm_tabs_prefix to the dispatch table. -
python/iterm_tab_names.applescript– the original script. Unchanged; the Elixir module just wraps it.
Read lib/iterm_tabs.ex first. It is around 75 lines and shows the whole flow.
Limits to know
- macOS + iTerm2 only.
- The AppleScript walks the iTerm2 window list each call. For thousands of tabs this is slow; for the dozens any developer actually has, it is instant.
-
The matched tab is brought to the front (the AppleScript calls
tell application "iTerm" to activateandtell t to select). If you want a “background” send that does not steal focus, the AppleScript needs to be edited. -
The captured output is the visible buffer diff, not the command’s exit status. To get the exit status, append
; echo "EXIT=$?"to your command and parse the trailing line. - One Return per send. TUIs that submit on a different key (Shift+Enter, Ctrl+D) need the AppleScript adjusted.
When to reach for this vs the AppleScript directly
Use the AppleScript directly when:
- You are at the shell and want a one-liner.
-
You are scripting from
make,npm, a Bash alias, or anything that already speaks toosascript.
Use the Elixir module when:
- You want to call this from inside the running BEAM (a GenServer, a workflow step, a test harness).
- You want to expose it as an MCP tool with capability-based access control and audit logging.
-
You want the parsed
%{window, tab, name, profile}structure rather than a string blob.
The two are equivalent in capability. The Elixir module is the integration point; the AppleScript is the mechanism.