5.7 KiB
Antigravity CLI protocol
How internal/antigravity drives the Antigravity CLI, agy (brew install --cask antigravity-cli; agy --version prints the version, agy changelog the release notes, and agy --help the flags). The CLI is signed in once, interactively, by running agy with no arguments as the user byre runs as; agy models lists the models that login offers, and fails with "Please sign in" until then. The CLI updates itself.
Launch
agy --input-format stream-json --output-format stream-json \
--dangerously-skip-permissions --add-dir <session dir> \
[--model M] [--conversation ID] --print ""
--print ""puts the CLI in print mode; with--input-format stream-jsonit reads one message per line from stdin and runs a turn for each, in one conversation, until stdin closes. A bare-ptakes the next flag as its prompt, so the empty prompt is passed explicitly.--dangerously-skip-permissionssets the permission mode toalways-proceed; without it the mode isrequest-reviewand tool calls wait for approvals print mode can't give.--conversation IDresumes a conversation with its history; the model comes from--model, not from the conversation.- Commands the agent runs (
run_command) inherit the CLI's environment.
The session directory
There is no flag for a system prompt or MCP servers, so byre hands them over as a plugin. The plugin directory is passed as --add-dir. The CLI treats it as a second workspace and loads the plugin in its .agents/plugins/byre/:
plugin.json:{"name": "byre"}.rules/AGENTS.md: always-on guidance.rules/notes.mdusestrigger: always_onfrontmatter. The CLI cuts each rule file at 24,000 bytes, so longer guidance or notes must be refused before launch.mcp_config.json: one entry per server, with a Streamable HTTPurlor a stdiocommandandargs. The CLI namespaces plugin servers as<plugin>_<server>, so byre's own server isbyre_byre.hooks.json: handlers keyed by hook event, each a command that reads the hook's JSON from stdin and prints its reply, with atimeoutin seconds (default 30). Flat events (SessionStart,PreInvocation,PostInvocation,Stop) list handlers directly; tool events (PreToolUse,PostToolUse) wrap them in groups,[{"matcher": "*", "hooks": [...]}], wherematcheris a regex on the tool name.
The session's listener
Each session serves byre's MCP tools (Streamable HTTP, no SSE stream) and hooks on a 127.0.0.1 port. Every path starts with a random token made per session, written only into the plugin's mcp_config.json and hooks.json, so a page in a browser or another local process that can reach the port can't call byre's tools without reading those files.
The CLI loads MCP tool schemas lazily. The model reads a tool's schema from ~/.gemini/antigravity-cli/mcp/<server>/<tool>.json with view_file, then calls it through the built-in call_mcp_tool tool with ServerName, ToolName and Arguments.
Messages
User turn, written to stdin: {"event":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}. content may also be a plain string. Only text blocks are accepted: an image block fails the line with stream input content block type "image" is not supported, so byre saves each image to a file and adds a text block naming its path. A line without event or with event other than user fails or is ignored; control_request is "not supported yet", so there is no control channel.
Stdout, one JSON object per line, each {"event": E, E: {...}}:
initarrives at launch, before any input, withconversation_id,model,cwd, the built-intoolsandpermission_mode.step_update: one step of the conversation, bystep_index, withstate(ACTIVEwhile it runs, thenDONE) andstep_type:user_input: a user message, including one a hook injected.agent_response: model output.text_deltastreams its text acrossACTIVEupdates; theDONEupdate carriesduration_secondsandusage(input_tokens,output_tokens,thinking_tokens,cache_read_tokens,total_tokens). A response that only calls tools has no text. Thinking is counted but never streamed.tool: a tool call,tool_nameandtool_info(name,parameters, and onDONEoutput). A shell command isrun_commandwithCommandLine.system_message: a system note, such as the one after a resume.
resultends a turn:status(SUCCESSorERROR),response(the turn's last text),error,duration_seconds,num_turnsandusagetotals for the whole conversation, earlier processes included.
Interrupts and model switches
SIGINT ends the running turn: the CLI prints error: interrupted on stderr, sends a result with status ERROR and error interrupted, and exits. A tool step cut off this way gets no DONE update. Resume with --conversation after the old process exits to keep using the conversation.
There is no way to switch the model of a running process. Close the idle process's stdin, wait for it to exit, and restart with the new --model and the same --conversation.
Hooks
Hook input and output are camelCase JSON. byre uses PreInvocation, which runs before every model call with invocationNum (0 for a turn's first call) and initialNumSteps; an output of {"injectSteps":[{"userMessage":"..."}]} adds a user message the model reads on that call. byre also uses PreToolUse (input toolCall with name and args), whose output needs decision: allow, or deny with a reason the agent reads in place of the tool's result. The Stop hook gets no final message, so byre doesn't use it.
Forking
The CLI can't fork a conversation at a given point, so Antigravity sessions can't be forked.