Skip to content

Commit 5fb96c8

Browse files
committed
mcp(docs[instructions]): add hook, buffer, and is_caller disambiguators to _BASE_INSTRUCTIONS
why: agents waste turns asking for write-hook tools (not exposed by design), expecting a list_buffers affordance (privacy-contract declined), or reinventing a whoami tool (three layers already answer "where am I?"). Naming each boundary explicitly heads off the exploratory call. what: - Append HOOKS ARE READ-ONLY paragraph to _BASE_INSTRUCTIONS citing show_hooks / show_hook and pointing at the tmux config file for writes. - Append BUFFERS paragraph covering load_buffer/paste_buffer/ delete_buffer lifecycle, BufferRef tracking, and the OS-clipboard privacy reason for the list_buffers omission. - Extend the TMUX_PANE branch of _build_instructions with a one- sentence is_caller workflow pointer — only emitted when running inside tmux, since the sentence references the agent-context line. - Lock all three additions with new assertion tests.
1 parent 6ac1f41 commit 5fb96c8

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

src/libtmux_mcp/server.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,16 @@
6767
"wait_for_content_change (waits for any change). These block "
6868
"server-side until the condition is met or the timeout expires, "
6969
"which is dramatically cheaper in agent turns than capture_pane "
70-
"in a retry loop."
70+
"in a retry loop.\n\n"
71+
"HOOKS ARE READ-ONLY: inspect via show_hooks / show_hook. Write-hook "
72+
"tools are intentionally not exposed — tmux hooks survive process "
73+
"death, so they belong in your tmux config file, not a transient "
74+
"MCP session.\n\n"
75+
"BUFFERS: load_buffer stages content, paste_buffer delivers it into "
76+
"a pane, delete_buffer removes the staged buffer. Track owned "
77+
"buffers via the BufferRef returned from load_buffer — there is no "
78+
"list_buffers tool because tmux buffers may include OS clipboard "
79+
"history (passwords, private snippets)."
7180
)
7281

7382

@@ -118,7 +127,10 @@ def _build_instructions(safety_level: str = TAG_MUTATING) -> str:
118127
context += f" (socket: {socket_name})"
119128
context += (
120129
". Tool results annotate the caller's own pane with "
121-
"is_caller=true. Use this to distinguish your own pane from others."
130+
"is_caller=true. Use this to distinguish your own pane from "
131+
"others. To answer 'which pane/window/session am I in?' call "
132+
"list_panes (or snapshot_pane) and filter for is_caller=true — "
133+
"your pane is identified above. No dedicated whoami tool exists."
122134
)
123135
parts.append(context)
124136

tests/test_server.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,62 @@ def test_base_instructions_prefer_wait_over_poll() -> None:
161161
assert "wait_for_content_change" in _BASE_INSTRUCTIONS
162162

163163

164+
def test_base_instructions_document_hook_boundary() -> None:
165+
"""_BASE_INSTRUCTIONS explains hooks are read-only by design.
166+
167+
Without this sentence agents waste a turn asking for ``set_hook`` or
168+
trying to write hooks through a nonexistent tool. Naming the
169+
boundary heads off the exploratory call.
170+
"""
171+
assert "HOOKS ARE READ-ONLY" in _BASE_INSTRUCTIONS
172+
assert "show_hooks" in _BASE_INSTRUCTIONS
173+
assert "tmux config file" in _BASE_INSTRUCTIONS
174+
175+
176+
def test_base_instructions_document_buffer_lifecycle() -> None:
177+
"""_BASE_INSTRUCTIONS explains the buffer lifecycle + no list_buffers.
178+
179+
The load/paste/delete triple is non-obvious, and agents otherwise
180+
expect a ``list_buffers`` affordance. The instruction prevents both
181+
confusions and surfaces the clipboard-privacy reason so the
182+
omission reads as deliberate, not missing.
183+
"""
184+
assert "BUFFERS" in _BASE_INSTRUCTIONS
185+
assert "load_buffer" in _BASE_INSTRUCTIONS
186+
assert "paste_buffer" in _BASE_INSTRUCTIONS
187+
assert "delete_buffer" in _BASE_INSTRUCTIONS
188+
assert "BufferRef" in _BASE_INSTRUCTIONS
189+
assert "list_buffers" in _BASE_INSTRUCTIONS
190+
assert "clipboard history" in _BASE_INSTRUCTIONS
191+
192+
193+
def test_build_instructions_documents_is_caller_workflow_inside_tmux(
194+
monkeypatch: pytest.MonkeyPatch,
195+
) -> None:
196+
"""The is_caller workflow sentence appears only when inside tmux.
197+
198+
The sentence references "your pane is identified above", which is
199+
only true when ``TMUX_PANE`` is set and the agent-context line has
200+
been emitted. Outside tmux, the sentence would be a lie — so it
201+
lives inside the ``if tmux_pane:`` branch of ``_build_instructions``
202+
and must NOT appear in ``_BASE_INSTRUCTIONS`` itself.
203+
"""
204+
# Outside tmux: the workflow sentence must NOT appear.
205+
monkeypatch.delenv("TMUX_PANE", raising=False)
206+
monkeypatch.delenv("TMUX", raising=False)
207+
outside = _build_instructions(safety_level=TAG_MUTATING)
208+
assert "whoami tool" not in outside
209+
assert "is_caller=true" not in outside
210+
211+
# Inside tmux: the workflow sentence appears.
212+
monkeypatch.setenv("TMUX_PANE", "%42")
213+
monkeypatch.setenv("TMUX", "/tmp/tmux-1000/default,12345,0")
214+
inside = _build_instructions(safety_level=TAG_MUTATING)
215+
assert "is_caller=true" in inside
216+
assert "whoami tool" in inside
217+
assert "list_panes" in inside
218+
219+
164220
def test_build_instructions_always_includes_safety() -> None:
165221
"""_build_instructions always includes the safety level."""
166222
result = _build_instructions(safety_level=TAG_MUTATING)

0 commit comments

Comments
 (0)