Skip to main content

cleat

CI

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 resultsstdout, 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 stopsend_keys raises unless you pass confirm_password_prompt=True, which you should only do 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
possibly-awaiting-input best-effort (Linux only): looks like a plain read/cat-style canonical-mode prompt, not a busy program try send_keys with a line of input

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, exact) {stdout, exit_code, completed, state} (+ files_changed if watching, spoofed_marks if tampered, stdout_exact if exact=True) normal commands — full stdout, cleaned for readability
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). Despite spawning a non-login shell, both also source the login/profile files a real terminal's login shell would (.zprofile/.zlogin for zsh, .bash_profile/ .bash_login/.profile for bash) — so PATH/env set only there (e.g. Homebrew's brew shellenv in .zprofile) isn't silently missing.

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.

The nonce itself is embedded in a temp rcfile at session startup (bash's --rcfile, fish's -C 'source ...', zsh's $ZDOTDIR) — its path stays visible in the shell's own argv for the life of the process. cleat removes that file from disk as soon as the shell has read it (before it ever shows a prompt), so a same-uid child process spawned later in the session can't open the path and read the nonce out, even if it discovers it via /proc/<pid>/cmdline.

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_changed detects writes, not reads (create/modify/delete under the watched root). Read-tracking needs privileged syscall tracing.
  • stdout is cleaned, not byte-exact: it's ANSI-stripped and trimmed (a leaked leading newline, trailing whitespace) for readability, so leading blank lines or trailing whitespace the command itself printed are dropped. Pass exact=True to run_command for stdout_exact — ANSI-stripped only, preserving the command's real output exactly.
  • 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), not run_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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cleat-0.2.2.tar.gz (75.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cleat-0.2.2-py3-none-any.whl (48.3 kB view details)

Uploaded Python 3

File details

Details for the file cleat-0.2.2.tar.gz.

File metadata

  • Download URL: cleat-0.2.2.tar.gz
  • Upload date:
  • Size: 75.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cleat-0.2.2.tar.gz
Algorithm Hash digest
SHA256 5322c4dc17eb06f7f48c0acb14d814bf7022087514a319b04c3cf10e46768468
MD5 537ce2524c5282634605e2bbd678e261
BLAKE2b-256 5e8d7c7f839833588aafbbfa5f29227c3eeb0a65f0f148c7071aa749b891c885

See more details on using hashes here.

Provenance

The following attestation bundles were made for cleat-0.2.2.tar.gz:

Publisher: release.yml on sidyellur/cleat

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cleat-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: cleat-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 48.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cleat-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 fe9de587f8d804d3a526ad18109e8a61c3095e72e569298c61f9587652593f36
MD5 3afd1ec650c547b0b449faef7ea67827
BLAKE2b-256 541edf8b8d25728a19f888a16b50ef507a3b34b2b18f5e200bed607cbcdb4985

See more details on using hashes here.

Provenance

The following attestation bundles were made for cleat-0.2.2-py3-none-any.whl:

Publisher: release.yml on sidyellur/cleat

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 files

0.2.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page