Call the Google Antigravity CLI (agy) headlessly from any non-TTY context — Windows ConPTY + POSIX pty + MCP server. Fixes the empty-output bug (#76).
SaferSkills independently audited Agy Headless Bridge (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<!-- mcp-name: io.github.rhishi99/agy-headless-bridge -->
<div align="center">
agy) headlessly — and actually get output back.Codename PtyGravity · pty + antiGravity
📖 [Architecture & docs → rhishi99.github.io/agy-headless-bridge](https://rhishi99.github.io/agy-headless-bridge/)
</div>
agy -p "<prompt>" prints nothing when its stdout is not a real terminal. So calling it from a subprocess, an MCP server, CI, or another coding agent (Claude Code, Codex, …) returns an empty string and exit 0 — silently. This package gives agy a fresh pseudo-terminal so it emits normally, then cleans the output.
flowchart TB
subgraph B["❌ BEFORE — agy -p from any non-TTY caller"]
direction TB
a1["subprocess · MCP · CI · agent"] --> a2["agy -p "prompt""]
a2 --> a3["stdout gated by isatty()"]
a3 --> a4["(empty string)<br/>exit 0 · no error · no output"]
end
subgraph A["✅ AFTER — through agy-headless-bridge"]
direction TB
b1["subprocess · MCP · CI · agent"] --> b2["run(prompt)"]
b2 --> b3["allocate fresh pseudo-terminal"]
b3 --> b4["agy -p "prompt"<br/>isatty() == True"]
b4 --> b5["clean() strips ANSI/TUI"]
b5 --> b6["clean text ✓"]
end
classDef bad fill:#2a1313,stroke:#f87171,color:#ffd9d9;
classDef good fill:#0f2a1e,stroke:#34d399,color:#d7ffe9;
class a1,a2,a3,a4 bad;
class b1,b2,b3,b4,b5,b6 good;# ❌ The problem — plain subprocess
import subprocess
r = subprocess.run(["agy", "-p", "say hi"], capture_output=True, text=True)
print(r.stdout) # '' — prints nothing, exit 0
# ✅ The fix
from agy_headless_bridge import run
print(run("say hi")) # 'Hi! How can I help?'Three entry points around one core:
| Entry point | Invoke | Use for |
|---|---|---|
| Library | from agy_headless_bridge import run | embedding agy in Python |
| CLI | agy-bridge "prompt" | shell scripts, quick calls |
| MCP server | python -m agy_headless_bridge.mcp_server | letting an agent call agy as a tool |
agy gates its stdout on isatty(). The instant stdout isn't a terminal, it goes silent — no output, no error, exit 0:
$ agy -p "say hi" | cat
$ # empty. exit 0. nothing.The common winpty agy -p "..." workaround needs a terminal that already exists, so it still fails from any automated / non-TTY caller.
Allocate a brand-new pseudo-terminal (one that needs no parent tty) and attach agy to it. Same code path on every OS — only the pty allocator differs.
flowchart TD
A["Caller — non-TTY<br/>Claude Code · MCP · subprocess · CI"] -->|"prompt"| B{{"run(prompt)"}}
B --> C["find_agy()<br/>$AGY_PATH → PATH → OS defaults"]
C --> D{"sys.platform?"}
D -->|"win32"| E["pywinpty<br/>PtyProcess.spawn"]
D -->|"posix"| F["stdlib pty<br/>os.openpty + Popen"]
E --> G(["fresh pseudo-terminal"])
F --> G
G --> H["agy -p prompt<br/>isatty == True → emits"]
H -->|"raw bytes + ANSI/TUI chrome"| I["clean()<br/>strip CSI/OSC · collapse \r repaints · drop spinner glyphs"]
I -->|"clean text"| A| Platform | pty backend | Status |
|---|---|---|
| Windows | ConPTY via [pywinpty] (PtyProcess) | ✅ verified (agy 1.0.6) |
| Linux / macOS | stdlib [pty] (os.openpty + subprocess.Popen) | ✅ verified on macOS hardware |
[!TIP] Linux users wanted. The pty mechanics are verified on Linux CI and macOS hardware, but the realagyround-trip on bare-metal Linux hasn't been run. If you're on Linux:pip install agy-headless-bridge, try it, and tell us how it went — pass or fail. PRs welcome.
Why not just the existing `agy` Claude Code plugins? They wrapagyfor triggering (slash commands, model selection) but still callagy -pdirectly — so in any headless context they hit this exact empty-output bug. This package fixes the I/O layer they're missing. Use both together.
Before installing this bridge you need:
python --version.agy opens a browser OAuth flow), or setANTIGRAVITY_API_KEY in your environment if you use an API key.
agy -p "say hi" should print a reply.(From a pipe it won't — that's the very bug this package fixes.)
pywinpty (installed automatically as a dependency).POSIX uses the stdlib pty module — nothing extra.
This package does not install or authenticateagy, and does not bundle any credentials. It only spawns theagyalready on your machine.
Requires Python 3.9+.
pip install agy-headless-bridge # pywinpty auto-installs on Windows onlyFrom source:
git clone https://github.com/rhishi99/agy-headless-bridge
cd agy-headless-bridge
pip install -e .The bridge locates the binary via, in order: $AGY_PATH → agy on PATH → OS default install paths.
from agy_headless_bridge import run, AgyNotFoundError
try:
print(run("reply with exactly: OK", timeout=60))
except AgyNotFoundError:
print("install agy first")run(prompt, timeout=180, agy_path=None) -> str — raises AgyNotFoundError if the binary is missing, TimeoutError on timeout, ValueError on empty prompt. Returns "" only if agy genuinely emitted nothing.
agy-bridge "reply with exactly: OK"
python -m agy_headless_bridge "reply with exactly: OK" # equivalentclaude mcp add --transport stdio antigravity -- \
python -m agy_headless_bridge.mcp_serverWindows: usepy -3.11(the [Python Launcher]) instead ofpythonin the command above. Barepythoncan resolve to the wrong interpreter or the Windows Store stub, which surfaces as an-32000MCP connection error. See Troubleshooting.
The server speaks JSON-RPC stdio directly (no MCP SDK dependency) and routes every call through the pty bridge.
Tool schema (what an agent — or you, integrating manually — sees):
| Tool | Argument | Type | Required | Description |
|---|---|---|---|---|
agy_ask | prompt | string | ✅ | one-shot prompt sent to agy |
agy_research | query | string | ✅ | wrapped as a deep-research prompt for agy |
Response shape — a standard MCP tools/call result; the answer is the text content:
{
"jsonrpc": "2.0",
"id": 2,
"result": { "content": [ { "type": "text", "text": "<agy's cleaned answer>" } ] }
}On failure the text is an [agy-mcp] ERROR: ... string (agy missing, timeout, etc.) rather than a JSON-RPC error, so the agent always gets a readable reply.
The whole point: let one AI coding tool delegate work to Gemini via Antigravity, headlessly. Common setups:
| Use case | How |
|---|---|
| Claude Code asks Gemini for a second opinion / diff review | MCP server → agy_ask tool |
A CI step runs an agy prompt and captures the answer | agy-bridge "..." in the workflow |
| A Python pipeline fans work out to agy | from agy_headless_bridge import run |
| Codex / any MCP-capable agent delegates to agy | register the same MCP server |
| Cron / scheduled job summarizes logs via agy | agy-bridge in the script |
Register the MCP server, then prompt Claude to use it:
claude mcp add --transport stdio antigravity -- \
python -m agy_headless_bridge.mcp_serverWindows: swappythonforpy -3.11here too — see the note above and Troubleshooting.
Prompt to Claude Code: "Use the `agy_ask` tool to ask Antigravity to review this function for edge cases, then summarize its findings for me."
If you also want slash-command triggering and model selection, pair this bridge with the community antigravity-cc Claude Code plugin — that handles the /agy:* commands and Gemini/Claude model swap; this handles the headless I/O.
Add the server to the client's MCP config:
{
"mcpServers": {
"antigravity": {
"command": "python",
"args": ["-m", "agy_headless_bridge.mcp_server"]
}
}
}Prompt to the agent: "Call `agy_research` with the query 'idiomatic error handling in Rust' and turn the result into a checklist."
ANSWER="$(agy-bridge 'Summarize the key risk in this diff in one sentence.')"
echo "$ANSWER"| Env var | Default | Meaning |
|---|---|---|
AGY_PATH | auto-detect | Absolute path to the agy binary |
AGY_BRIDGE_TIMEOUT | 180 | Seconds before a call is killed |
clean() worksagy's pty output is a TUI stream, not plain text. clean() removes ANSI escapes (CSI/OSC — colors, cursor moves), `\r` repaints (a spinner overwrites one line; only the final paint is kept), and box-drawing / spinner glyphs (╭─╮ │ ⠋⠙⠹) — leaving just the model's answer.
What comes off the pty vs. what you get back:
RAW (off the pty) CLEANED (returned to you)
───────────────────────────────────── ─────────────────────────
⠋ thinking…\r⠙ thinking…\r\x1b[2K A closure is a function that
\x1b[32m╭─────────────╮\x1b[0m captures variables from the
\x1b[32m│\x1b[0m A closure is a function scope where it was defined.
that captures variables from the
scope where it was defined.
\x1b[32m╰─────────────╯\x1b[0m`pip install` fails on Windows building `pywinpty` — pywinpty is a native extension. If pip tries to build from source and errors with a compiler/cl.exe message, install the Microsoft C++ Build Tools (or use a Python where a prebuilt pywinpty wheel exists — recent CPython on Windows has them). Upgrade pip first: python -m pip install -U pip.
`-32000` MCP connection error on Windows (interpreter mismatch) — bare python in the claude mcp add command can resolve to the wrong interpreter or the Windows Store stub, so the spawned server can't import the package and the connection fails. Use the [Python Launcher] instead — register the server with py -3.11 (matching the Python where you installed the package) in place of python. To confirm where the package actually landed, run pip show agy-headless-bridge and check the Location: field; if it points to a different Python than Claude Code spawns, that mismatch is the cause.
`AgyNotFoundError` — the bridge can't find agy. Set AGY_PATH to the absolute path of the binary, or make sure agy is on your PATH (agy --version should work in your shell).
Empty string returned — agy produced no output. Confirm it works in a real terminal first: agy -p "say hi". If that's also empty, the problem is agy/auth, not the bridge. Re-authenticate (agy interactively) or check ANTIGRAVITY_API_KEY.
`TimeoutError` — the call exceeded AGY_BRIDGE_TIMEOUT (default 180s). Raise it for long prompts: AGY_BRIDGE_TIMEOUT=600 agy-bridge "..." or run(prompt, timeout=600).
Pseudo-terminal allocation fails — rare. On Windows it means pywinpty isn't importable (reinstall it). On POSIX it means the system is out of pty slots or pty.openpty() is denied (containers with no /dev/pts); run with a real pty available.
Garbled / partial output — open an issue with the OS, Python + agy version, and the raw output; clean() may need another glyph rule.
pip install -e ".[dev]"
pytestUnit tests (cleaning, arg validation, binary discovery) always run. The live agy round-trip test auto-skips when agy isn't installed — so CI runners (which don't have agy) stay green and never need credentials. CI runs on Windows + Linux across Python 3.9 and 3.12.
here — it's an agy settings.json concern, covered by the antigravity-cc plugin. Pair the two.
agy, and ships no credentials.You are responsible for using agy within Google's terms of service. This project only changes how stdout is captured — it does not bypass auth, quotas, or any access control.
MIT.
[#76]: https://antigravity.google/cli [pywinpty]: https://github.com/andfoy/pywinpty [pty]: https://docs.python.org/3/library/pty.html [Python Launcher]: https://docs.python.org/3/using/windows.html#python-launcher-for-windows
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.