ItermTabs: an Elixir Module for Driving iTerm2 from MCP and IEx

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:

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:

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:

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:

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:

Read lib/iterm_tabs.ex first. It is around 75 lines and shows the whole flow.

Limits to know

When to reach for this vs the AppleScript directly

Use the AppleScript directly when:

Use the Elixir module when:

The two are equivalent in capability. The Elixir module is the integration point; the AppleScript is the mechanism.