Skip to main content
Glama
README.md
# meatproxy

A coding agent sometimes needs something only you have: a token, a sudo
password, your corrections to a text it wrote. meatproxy is an MCP server that
asks you for it in a window on your own screen. A secret goes from that window
straight to the one command that needs it, and the agent never sees it. Text you
write or fix goes back to the agent.

[Русская версия](README.ru.md)

![The agent asks for a GitHub token; a tmux popup shows the command that will receive it](docs/img/secret-empty.png)

## Why "meat proxy"

The term comes from [meatproxy.me](https://meatproxy.me): a person who only
relays text between an AI and the world. When an agent hits a password prompt,
you become its meat proxy. You copy the token into the chat, type the sudo
password, retype the commit message. The token stays in the transcript and in
the provider's logs, and the command it unlocked went by in a scroll of tool
calls you half read.

meatproxy_mcp gives the meat proxy a bit more control. Every request opens a
window that names who asked, the exact command, its directory and how the
value will reach it. You answer there or close the window. The value goes to
the command's standard input or to one environment variable, and the agent
gets the exit code and the output with the value replaced by `[secret]`.
Nothing is stored: the next call asks again.

## What you can use it for

### An API key or a token

The agent calls `run_with_secret`:

```json
{"purpose": "log gh in to github.com", "argv": ["gh", "auth", "login", "--with-token"], "via": "stdin"}
```

A tmux popup opens over your terminal with the request under an empty first
line. You paste the token there and write the file (`:wq`).

![The same popup with a token pasted on the first line](docs/img/secret-filled.png)

The agent gets back `{"status": "ran", "exit_code": 0, "timed_out": false, "stdout": "...", "stderr": "..."}`.
For a program that reads its key from the environment, the agent passes
`"via": "env", "env_name": "NODE_AUTH_TOKEN"`. Outside tmux the same request is
a GTK dialog with hidden input:

![zenity dialog with hidden input for an npm token](docs/img/zenity-entry.png)

### Text in your own editor

Typing a paragraph into a chat box is slow, and the agent's drafts need
editing. `ask_user` opens the draft in your `$EDITOR` (nvim, helix, micro,
anything). What you leave above the marker line goes back to the agent. Use it
for a letter, a PR description, a list of hosts, a config the agent should
follow.

![A reply draft open in nvim inside a tmux popup](docs/img/shared-draft.png)

`ask_user_async` returns at once with a `reply_id`, so the agent can keep
working while you write; `read_reply` collects the text later. Without tmux the
draft opens in a GTK text window:

![zenity text window with the same draft](docs/img/zenity-text.png)

### Commit messages

A git `commit-msg` hook can hand the agent's message to you before the commit
happens. It opens in the same popup with the staged files listed under it.

![The agent's commit message open for review, with the diff stat below](docs/img/commit-msg.png)

| You do | The commit |
|---|---|
| edit if needed, `:wq` | goes ahead with your text |
| `:cq`, or delete the text and `:wq` | is aborted; the agent is told to ask you what to change |
| nothing for 100 s | goes ahead with the agent's text |

[`examples/git-hooks/commit-msg`](examples/git-hooks/commit-msg) is the hook
this setup runs. It acts only when `CLAUDECODE=1` (Claude Code sets it for the
commands it runs) or `HERMES_AGENT=true`, so your own commits are left alone.
Install it as a global hook:

```
mkdir -p ~/.config/git/hooks
cp examples/git-hooks/commit-msg ~/.config/git/hooks/
git config --global core.hooksPath ~/.config/git/hooks
```

`core.hooksPath` replaces each repository's `.git/hooks`. The example ends by
calling the repository's own `commit-msg` hook. Other hooks in `.git/hooks`
(`pre-commit`, `pre-push`) stop running unless you put a stub with the same
last lines next to it for each one. A repository that sets its own
`core.hooksPath`, as husky 5 and later and lefthook do, never runs the global hook. The
core of the hook:

```sh
meatproxy-edit "$msg" --purpose "commit message for $(basename "$top")" --caller agent \
	--note-file "$stat" --file-name COMMIT_EDITMSG --wait "$wait_seconds"
case $? in
0) ;;        # written back: commit with the user's text
1) exit 1 ;; # :cq or emptied: abort the commit
*) ;;        # 5 timed out, 2 no window: commit the message as written
esac
```

`touch ~/.local/state/commit-review-off` turns the review off;
`COMMIT_REVIEW_WAIT` changes the 100 seconds.

### sudo, here and over ssh

`run_with_sudo(argv, server)` runs one command under sudo on this machine or
on a host from `~/.ssh/config` or an ssh-manager list. The window names the
host, the login and the target user. meatproxy feeds the password to `sudo -S`
itself, so it never appears in an argument list or on the remote command line.

![Password window for sudo on a remote host over ssh](docs/img/sudo-ssh.png)

`meatproxy-askpass` draws the same window for plain `sudo -A`, from your shell
or the agent's. It reads the calling sudo from `/proc` and marks commands that
are hard to undo:

```
export SUDO_ASKPASS=/usr/local/bin/meatproxy-askpass
sudo -A systemctl stop nginx
```

![sudo -A asking through meatproxy-askpass, with a warning for a destructive command](docs/img/askpass.png)

It refuses to start unless its parent is sudo. Otherwise an agent could run it
directly and read the password from its output.

## Tools

| Tool | Window says | What comes back to the agent |
|---|---|---|
| `run_with_secret(purpose, argv, via)` | **SECRET**: the value goes to the stdin or to one variable of the command shown, and is not shown to the agent | exit code, stdout, stderr; the value replaced by `[secret]` |
| `run_with_sudo(argv, server)` | **SECRET**: sudo reads the password to run the command shown, here or on the named host over ssh | exit code, stdout, stderr; the password replaced by `[secret]` |
| `ask_user(purpose, draft)` | **SHARED**: what you leave in the window is sent to the agent | your text |
| `ask_user_async(purpose, draft)` | the same as `ask_user` | a `reply_id` at once |
| `read_reply(reply_id, wait_s)` | | your text, or `pending` |

One call opens one window and runs one command.

## Input methods

Tried in the order of `backends` in the config; the first that can open a
window is used.

| Method | When | Window |
|---|---|---|
| `exec` | you configured a program | yours: one JSON line in, one out |
| `tmux` | the agent runs inside tmux | popup over the attached client, or a tmux window |
| `zenity` | a display and zenity | hidden-input dialog, or an editable text window |
| `terminal` | a display, no tmux | a terminal with a hidden prompt or your editor |

The tmux and terminal windows open `editor` from the config, or `$VISUAL`,
then `$EDITOR`, then nvim. vim, nvim, emacs, micro, nano, helix and kak start
with flags that keep the value out of swap and history files. Adding a method,
including one for Windows, is described in the
[backend guide](docs/en/backend-guide.md).

Only vim and nvim can quit with an error (`:cq`). In any other editor, such as
nano, quitting without saving sends the text as it was. To refuse there, delete
the text, save and quit. A secret then is not sent, and a draft or a commit
message is cancelled.

## Install

You need Linux and Python 3.12 or newer. To draw the window, you also need at
least one of these: tmux 3.3 or newer, zenity, or a terminal emulator in a
graphical session. The terminal is `xdg-terminal-exec` by default; set
`terminal` in the config to use another one.

```
git clone https://github.com/SilentAutomaton/meatproxy_mcp.git meatproxy
cd meatproxy
python3 -m venv .venv && .venv/bin/pip install -e .
mkdir -p ~/.config/meatproxy
cp config.example.toml ~/.config/meatproxy/config.toml   # optional
```

Register it with your MCP client as a stdio server. For Claude Code, from the
same directory:

```
claude mcp add --scope user meatproxy -- "$PWD/.venv/bin/meatproxy-mcp"
```

`claude mcp list` should show `meatproxy` as connected. To check the window,
start a new session and ask the agent to call `ask_user`.

The askpass helper and the editor for hooks are two launchers in `bin/`. They
run on the system `python3`, which must also be 3.12 or newer, and need nothing
from the venv. `meatproxy-askpass` reads `/proc`, so it works only on Linux.

```
sudo ln -s "$PWD/bin/meatproxy-askpass" "$PWD/bin/meatproxy-edit" /usr/local/bin/
```

For `sudo -A`, put the `export SUDO_ASKPASS=/usr/local/bin/meatproxy-askpass`
line in the startup file of the shell you start the agent from (`~/.zshrc`,
`~/.bashrc`). Then your own commands and the agent's both find the helper. The
commit-msg hook is optional; [Commit messages](#commit-messages) shows how to
install it.

The tools wait for you. Claude Code lets a tool call run for about 28 hours by
default; if `MCP_TOOL_TIMEOUT` or a per-server `timeout` is set lower, keep it
above `timeout_s` (both are in milliseconds). More on the helpers in
[system integration](docs/en/system-integration.md).

## What it does not protect against

Programs running as your own user, a command you approve without reading, and
plugins in your editor config. The [threat model](docs/en/threat-model.md)
lists all of it.

## Similar projects

- [1Password Environments MCP](https://www.1password.dev/environments/mcp-server.md)
  and `op run`: stored secrets injected into a process, the agent sees names only.
- [secret-gate](https://github.com/johnuopini/secret-gate): approval over
  Telegram, secrets from 1Password injected as environment variables.
- [agent-secrets](https://github.com/Brissux-Labs/agent-secrets): a secret
  broker with a hidden prompt or a one-time web form, and output redaction.
- [human-mcp](https://github.com/LIghtJUNction/human-mcp): blocking and async
  questions to a human; the async pair here follows its idea.

meatproxy stores nothing, asks you on every use, shows the exact command
that will receive the value, and draws the window where you already work.

## License

MIT