A headless terminal layer: a persistent PTY + OSC 133 structure source, exposed to AI agents over MCP.
Project description
cleat
A headless terminal layer for AI agents. cleat runs a persistent shell
session behind a PTY, parses its byte stream for OSC 133
shell-integration marks, and exposes it to an agent over MCP
as structured results — stdout, real exit_code, files touched — instead
of raw escape-code soup.
It is not a terminal emulator and not a modification to your terminal. It's a separate process that runs inside whatever terminal you already use, and it stays terminal-agnostic.
Why
When an agent drives a terminal, it normally sees a continuous byte river:
prompt redraws, echoed keystrokes, color codes, and output, with no marker for
where one command's output ends or whether it succeeded. Worse, when you scrape
a PTY, the exit code isn't in the stream at all — the shell knows $? but
never prints it.
cleat injects OSC 133 marks into the shells it spawns and parses them back
out, so the agent gets:
{ "stdout": "...", "exit_code": 0, "completed": true, "state": "idle" }
…and the session is persistent: cd, export, activated venvs, and ssh
sessions all carry across calls — something a fresh subprocess per command
cannot do.
Session state
Every tool response carries a state field, derived from termios flags and
the foreground process group — facts only the process holding the PTY can
read — instead of guessed from output timing:
state |
Meaning | What to do |
|---|---|---|
idle |
nothing is running | call run_command for the next thing |
running |
a command is executing | poll read_output |
awaiting-input |
a REPL/prompt is blocked on stdin | drive it with send_keys |
password |
a secret prompt (sudo, read -s) is waiting, echo off |
stop — only send input here with the human's explicit consent |
tui |
a full-screen program (vim, top, less) owns the terminal | use read_screen/send_keys, not run_command |
Install
Requires Python ≥3.10 on a POSIX system (Linux/macOS). cleat is on
PyPI.
Register it with Claude Code — with uv (no install step needed):
claude mcp add cleat -- uvx cleat
…or if you'd rather install it into your environment first:
pip install cleat
claude mcp add cleat -- cleat
Either way, you can add it to a project's .mcp.json:
{
"mcpServers": {
"cleat": {
"command": "uvx",
"args": ["cleat"]
}
}
}
To run the latest unreleased code straight from git instead of PyPI, use
uvx --from git+https://github.com/sidyellur/cleat cleat.
For local development:
git clone https://github.com/sidyellur/cleat && cd cleat
python -m venv .venv && . .venv/bin/activate
pip install -e .
cleat # runs the MCP server over stdio
Tools
| Tool | Returns | Use for |
|---|---|---|
run_command(command, timeout) |
{stdout, exit_code, completed, state} (+ files_changed if watching, spoofed_marks if tampered) |
normal commands — full, exact stdout |
read_output(timeout) |
{output, exit_code, completed, state} |
watching a long-running / streaming command |
wait_for(timeout) |
{output, exit_code, completed, state} |
blocking until the session needs attention — replaces a read_output polling loop |
read_screen() |
{screen, cursor, state} |
inspecting a full-screen TUI (vim, top, less) |
send_keys(keys, enter) |
{screen, cursor, exit_code, completed, state} |
driving a REPL / TUI / prompt (control chars pass through: =Ctrl-C) |
resize(cols, rows) |
{cols, rows} |
laying out a TUI for a given size |
watch_files(path) |
{watch_root} |
enable per-command files_changed under path |
completed: false means the program is still running or waiting for input (e.g.
a REPL) — check state (above) for what to do next.
How it works
agent ─(MCP)─ server.py ─ engine.py (persistent PTY, ptyprocess)
├─ structure.py → OSC 133 marks → {stdout, exit_code, state}
└─ pyte screen → rendered view for REPLs/TUIs
The engine injects OSC 133 per shell without touching your real config: zsh via
a temp $ZDOTDIR, bash via --rcfile + vendored bash-preexec,
and fish via fish -C 'source ...' (fish ≥4 also emits its own native marks
alongside ours; see below for why that’s harmless).
Nonce-authenticated marks
A command you run can print arbitrary bytes to its own stdout — including a
fake ESC ]133;D;0 BEL, forging a clean exit code for output that actually
failed. cleat closes this: each session gets a fresh secrets.token_hex(8)
nonce at startup, embedded in every mark cleat injects
(\033]133;D;<exit>;k=<nonce>\007). A mark whose k= param is missing or
wrong is ignored outright — it cannot alter exit_code, stdout, or
completed either way. A wrong k= is always surfaced as an attempted
forgery; a missing k= is too, unless the shell is known to run its own
legitimate native OSC 133 alongside ours (fish ≥4) — that's expected
telemetry, not tampering, so it's ignored quietly instead of triggering a
false alarm on every fish command. run_command surfaces the real count as
spoofed_marks in its result (only when it's nonzero), so an agent can tell
when a program it ran tried to lie about how it finished.
Caveats
- POSIX only. Uses
pty/termios; no Windows. - It gives the agent a real shell. Commands run with your user's privileges in a persistent session. Run it only where you'd let an agent run shell commands.
files_changeddetects writes, not reads (create/modify/delete under the watched root). Read-tracking needs privileged syscall tracing.- bash: a command whose first token is a subshell
(...)or brace group{ ...; }emits no start mark; its exit code is recovered but stdout for that one command is not captured. - Full-screen TUIs: use
read_screen(the rendered grid), notrun_command. - Shells: zsh and bash are fully supported, including interactive REPL/TUI driving. fish is supported for command execution on ≥4 (its own native marks require that version; cleat's injection works alongside them either way — see Nonce-authenticated marks).
License
MIT (see LICENSE). Vendors bash-preexec, also MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cleat-0.2.1.tar.gz.
File metadata
- Download URL: cleat-0.2.1.tar.gz
- Upload date:
- Size: 59.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5aabbf0852c39d15254eb135993e1a7139989864b5696865ccd921869f6b1568
|
|
| MD5 |
e49c931ce05022c0bca2301f0be97912
|
|
| BLAKE2b-256 |
478f5ee172e95b759b34c32d1654630ee8bd37276588460d4a6c7552deaee431
|
Provenance
The following attestation bundles were made for cleat-0.2.1.tar.gz:
Publisher:
release.yml on sidyellur/cleat
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cleat-0.2.1.tar.gz -
Subject digest:
5aabbf0852c39d15254eb135993e1a7139989864b5696865ccd921869f6b1568 - Sigstore transparency entry: 2065676130
- Sigstore integration time:
-
Permalink:
sidyellur/cleat@2f321be203473865a6e247a8a18103a7834efe2a -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/sidyellur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2f321be203473865a6e247a8a18103a7834efe2a -
Trigger Event:
push
-
Statement type:
File details
Details for the file cleat-0.2.1-py3-none-any.whl.
File metadata
- Download URL: cleat-0.2.1-py3-none-any.whl
- Upload date:
- Size: 38.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1eac3f64f7ce6ad2e7962b95871582ea70ebce032c7b72be57c855cb25223b32
|
|
| MD5 |
9998f5d071fcb63abe2e2cb0a8bb36ce
|
|
| BLAKE2b-256 |
c122290053e2b73bd1c57363f1e87c06a6d5cf6773aa697f5b0868307b448bc2
|
Provenance
The following attestation bundles were made for cleat-0.2.1-py3-none-any.whl:
Publisher:
release.yml on sidyellur/cleat
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cleat-0.2.1-py3-none-any.whl -
Subject digest:
1eac3f64f7ce6ad2e7962b95871582ea70ebce032c7b72be57c855cb25223b32 - Sigstore transparency entry: 2065676226
- Sigstore integration time:
-
Permalink:
sidyellur/cleat@2f321be203473865a6e247a8a18103a7834efe2a -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/sidyellur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2f321be203473865a6e247a8a18103a7834efe2a -
Trigger Event:
push
-
Statement type: