All notable changes to Antigravity for Claude Code. Format loosely follows
Keep a Changelog; versions are in .claude-plugin/plugin.json.
- agy-delegate.sh no longer burns a CPU core on large outputs on macOS. The two
whitespace-emptiness checks stripped every whitespace character from the whole output
(
${OUT//[...]/}) just to test if anything was left — on the bash 3.2.57 that macOS ships, that gets drastically slower as output grows (measured: 26.6 s at 8 KB; two real wrappers spun at ~99% CPU for 24 min and 2+ hours after agy had already finished). Both checks are now a glob that stops at the first non-whitespace character, with the same whitespace set and unchanged exit-code behavior (#66). - The same shape was live in
cloud-debug.sh, and worse. Found by review of the fix above, not by the fix.${LOGS//[[:space:]]/}decided "did gcloud return any logs?" on the rawgcloud logging readoutput — and the 200 KB cap sits after that line, so a chatty service could pin a core before the cap ever ran. Measured on 3.2.57: the POSIX class spelling costs the same as the ANSI-C one (23.8s vs 24.4s at 8 KB), so the first static guard, which matched only the ANSI-C form, reported all-clear over it. A cheap glob now runs first and the strip is reached only when the string is already nothing but whitespace and brackets — exactly equivalent, checked on 19 probes. - Why 305 tests on a bash-3.2 machine saw none of this. The cost switches on whether
there is a match at all, not on how many: same 8 KB, 0% whitespace 0.04s, 2% or more
21-25s. Both large fixtures in the suite sat on the fast side — the delegate's
bigis solidxand$( )strips its one trailing newline, and the gcloudbigmode is solidAinside JSON. Neither contained a single space. Both now have whitespace-bearing counterparts, and both new tests kill the run at 30s rather than waiting it out, because bash cannot service SIGTERM inside the substitution:timeout 90around the unfixed wrapper returned after 205s, which is also whyagy-job statuskept sayingrunning.
The --sandbox follow-up 0.25.0 deferred, now that agy runs again — and the answer is no.
--sandboxis not containment, and four documents were recommending it as such. 0.25.0 held it back because agy was failing every run with an eligibility error and this repository does not ship behavioural changes it cannot measure. Measured now, on macOS with agy 1.1.19: with--yolo, the flag changes nothing. A write to an absolute path outside--dirsucceeded (rc 0, 8 bytes, verified by content),idran and returned a real uid, andcurl https://example.comreturned 200 — identical with and without it. agy's own--helpsays "terminal restrictions"; whatever it restricts, it is not those, not in this combination. Not tested on Linux, and the claim is scoped to what was run. Withholding it in 0.25.0 turned out to be the right call for the wrong reason: it would have been a flag that reads as containment and provides none, which is the exact shape this repository keeps having to remove.- The warning 0.25.0 added to
agy-mediaunderstated the exposure. It said--direxposes the containing directory. The same measurement shows--diris not a boundary — it is where agy starts looking.--yolois a grant over the whole machine, and the message says that now. - Two guards, both mutation-verified: no user-facing file may recommend
--sandboxas containment, and the media warning must say the grant covers the machine. The containment rule needed five shapes; four were killed by a mutation, not by reading. Matching per line exempted any line containing "is not" — and the measurement sentence pasted after the claim says "it is not those", so re-adding "adds containment" passed. Per line with the negation required adjacent to the word fixed that and then missed a claim split across a wrap, which is how prose is written. Two-line windows fixed the wrap and then exempted a bad sentence sitting beside a good one, because the neighbour's negation satisfied the whole window.tests/check-sandbox-claims.pyjudges sentences, so each claim carries its own negation or none — and adjacent PAIRS are judged too, after a fourth mutation showed a claim can be spread across two sentences ("Add--sandboxfor isolation. It contains the untrusted commands."), which neither half trips alone. Both passes run and neither subsumes the other. The negation also accepts contractions: requiring the literal word would have flagged "--sandboxdoesn't contain the agent", a correct sentence, which is the opposite failure and the one that gets a checker deleted. It does NOT catch a claim spread over three or more sentences, and that is left alone on purpose: a window of N is beatable at N+1, so widening is a race the checker cannot win, and each widening adds false-positive surface. It guards against drift; it is not a proof. All six shapes are pinned by the checker's own tests — and the contraction case had to be rewritten, because its first version said "does not contain anything; it doesn't contain the agent", where the bare "not" matched first and the case passed with contraction support deleted outright. Both reviewers caught that independently. - The comments in
agy-media.shand the test block still framed the exposure as the containing directory, directly above the new text saying the opposite, andSKILL.md's recipes still passed--yolo --sandbox— teaching a flag the same file had just called useless. Both found in review.
298 -> 305.
Fixes GHSA-hwv2-vjgj-8rcv (CVSS 8.6), reported privately by @Valkyness with
non-destructive, exit-code-only proofs for every claim. SECURITY.md names
hooks/validate-delegate-bash.sh as the only control on what the prompt-injectable
antigravity-delegate subagent may run, so a bypass there is the highest-severity class
this project has. Three of them were open at once.
-
The gate authenticated a command by BASENAME.
base()reduced the first token to its filename, so any executable calledagy-delegate,agy-job, or an allowed producer passed from anywhere:./agy-delegateout of a cloned repository,/tmp/evil/agy-job. Untrusted repository content is the exact prompt-injection sourceSECURITY.mdnames, so the control was defeated by the checkout it exists to survive. A wrapper must now be a bare name — no/, no\. Nothing needed a path:agents/,commands/andskills/have invoked these by bare name since 0.14.0, because the plugin putsbin/on the Bash tool PATH and$CLAUDE_PLUGIN_ROOTis not exported to model-run Bash (issue #11). A bare name resolves through PATH; a path resolves through the working directory, which an attacker controls. -
The producer allowlist is gone entirely, which closes the other two findings at once. It was the second half of the #29 hardening, kept so
git diff | agy-delegate -would work, and that convenience was the hole:gitwith unrestricted arguments is a living-off-the-land binary.git -c alias.x='!cmd' xrunscmd; so dogit --exec-path=<dir>andgit -c core.pager=<cmd>. The last two are not in the advisory — they turned up while reproducing it, which is the point: the defect is allowing a command by NAME while ignoring its ARGUMENTS, not any one flag.git push --force,git clean -fdxandgit reset --hardwere reachable through the same slot.cat,echoandprintffeedingagy-delegate -were a file and secret exfiltration primitive. The scanner blocks$(but allows$VAR, and the right-hand side sends stdin to the external model as the prompt:cat ~/.ssh/id_ed25519 | agy-delegate -andprintf %s "$AWS_SECRET_ACCESS_KEY" | agy-delegate -were both allowed, with no file allowlist and no confirmation.
Nothing needed the pipeline. This subagent's own contract already said the gate "blocks every Bash command except the delegation wrapper" and showed only
agy-delegate [options] "<task>"; the documentedgit diff | agy-delegate --tier pro -incommands/review.mdruns as the main Claude, and this hook is registered in the agent frontmatter, so it never gated that. The gate is now what its own documentation always claimed. The denial says so and points at--dir <repo-root>instead. -
agy-medianow says what it is about to expose. The advisory flagged it as a contributing factor rather than a bypass: every real run passes--yolo(all tools, including terminal) together with--diron the media file's containing directory, so choosing one recording hands over everything beside it — which "transcribe this file" does not suggest. It prints the directory and how many entries are in it.--sandboxis the candidate narrowing and is not applied: agy on the maintainer's account currently fails every run withEligibility check failed, so it could not be measured, and this repository does not ship untested behavioural changes to working features.
12 regression tests, every one of the advisory's proofs among them, each verified to fail
against the unfixed gate. The two extra git vectors are included, and so is a liveness
assertion — a PoC harness that cannot produce an allowed command would let every deny
assertion pass on a broken payload, which has happened in this suite before.
- agy turned the write-without-grant denial into a hard error, and exit 15 quietly
stopped happening. Since 1.1.3 a denied tool in headless mode came back as rc 0 with
empty stdout and
auto-deniedon stderr; the wrapper matched that and returned 15 with the guidance that namespermissions.allow,--yoloandagy-doctor. By agy 1.1.13 the same denial fails the run: rc 1, andpermission check failed for write_file "...": user denied permission for write_file(...)— none of the old anchors, and it lands in the rc != 0 branch, above the soft-deny check entirely. So the single most documented failure in this plugin (issue #10) came back as a bareagy exited 1. Both shapes now route through one function, so they cannot drift apart again. Measured, not inferred: a plain write and--mode accept-editswere each run against a real 1.1.13 and both produced the hard error; after the fix the same run returns 15. 0.22.5 said this path was intact. It checked thatauto-deniedandpermissions.allowwere still present in the agy binary and concluded the classifier was safe. The strings were there. The route was not — agy no longer takes it. Verifying an anchor is not verifying that anything still reaches it. - The
flashtiers move to Gemini 3.7 Flash (High) / (Low). 3.6 and 3.7 are priced identically, and both undercut the 3.5 this plugin has defaulted to since the beginning on every axis: input and cached-input are exactly half ($1.50 -> $0.75, $0.15 -> $0.075) and output is cheaper still, $9.00 -> $3.75 — a 58% cut, not a halving, which the first draft of this entry got wrong in three files. That is promotional pricing which ends 2026-12-31, after which they settle at $1.50 / $7.50 / $0.15 — still cheaper than 3.5 on output, identical on the rest. Checked on 2026-08-17 against ai.google.dev and Google Cloud's Agent Platform page. No quality claim is attached. The reason is price and currency; this repo has already retracted a 3.5-vs-3.6 comparison for being measured on a build where--modelwas ignored, and nothing has been re-measured since. The old default was justified on "broad plan availability", which still argues the other way for a model four days old — but that failure is loud, not silent:doctorwarns the tier model is absent fromagy models, and a delegation exits 14 naming the fix. 3.6 is a cost-identical remap.prices.jsongainsgemini_flash_35/36/37plus_post_2026entries carrying the rates that take over on 2027-01-01, and the note says which keygemini_flashcurrently mirrors. The old note claimed 3.6's "input and cached-input unchanged" — true only after the promotion ends; today both are half. doctorvalidates the rules agy RESOLVED, not the one file it used to read. 0.22.5 addedpermissions.allowvalidation by parsing~/.gemini/antigravity-cli/settings.json. agy applies more than that: asharedscope lives in~/.gemini/config/config.json, so a broken rule there was reported clean — a check that says all-clear about a file it never opened. agy 1.1.12 answers-p /permissionswith one<scope>\t<action>\t<rule>record per line, no agent turn and no tokens, so doctor stops guessing which files to open. Below 1.1.12, and whenever the answer comes back empty, it falls back to the file — empty is also what a hang looks like, and the difference between "nothing to report" and "nothing was looked at" is the whole point. On this machine the resolved view returns 13 rules where the file returned 12.--mode accept-editsis not a write grant, and the old explanation was unsound. Four places said it "auto-applied file edits headless on 1.1.0–1.1.2 but is soft-denied on 1.1.3". agy 1.1.12's own notes say--modewas ignored in headless-puntil it was fixed — so on the builds that claim was formed on, the flag was never applied, and the observation could not tell a denial apart from the flag doing nothing. Re-measured on 1.1.13, where it IS applied: the write is denied exactly like one without the flag. The conclusion survives; the reasoning behind it did not, and now says so.- The tier defaults are read from the wrapper in the test suite instead of written out
again. Moving
flashto 3.7 broke four assertions and two stub model lists that had the old name baked in, anddoctorkeeps its own copy of all three defaults — a mismatch there makes it warn that a tier model is missing while delegation happily uses a different one. The suite now derives all three frommodel_for_tier(), builds the stub's slug list from them, and asserts doctor's copies match. Theprices.jsoncheck likewise derives its key from the default rather than enumerating 3.5 and 3.6 and telling you to reconcile by hand for anything else — which is exactly what it did when 3.7 arrived. - Four things this release got wrong on the first pass, all caught in review. The
permissions.allowcheck was nested insideif [ -f settings.json ]— so on a machine configured only through thesharedscope, the one case it was written for, it ran no check and said nothing. The# Exit codes:header, which--helpprints verbatim, still described 15 as the 1.1.3 soft deny after this release made it cover both shapes. agy's own diagnostic printed twice on the hard-error path, because the rc != 0 branch dumps$ERRbefore classifying and the handler dumped it again. And "half of 3.5 on every axis" was wrong in three files: input and cached-input halve exactly, output goes $9.00 -> $3.75, which is 58%. Each now has a test, including the duplicate-output one, which had none until a mutation showed the fix could be reverted in silence. Then the terminology sweep missed files three separate times — README and SKILL.md still said an ungranted write leaves the run "succeeding" while TROUBLESHOOTING already said it fails; then POC-PLAYBOOK.md,commands/delegate.mdandagents/antigravity-delegate.mdwere found still describing exit 15 as the 1.1.3 soft deny alone, and so was theinfoline doctor actually prints. Reviewers caught each round. A grep would have caught all of them, so there is one now: any user-facing file that describes exit 15 must also name the 1.1.13 shape. FILE level rather than line level on purpose — a line rule needs exceptions for the version history, the subagent-spawn case and single-branch code comments, and a guard with three exceptions gets deleted. What actually went wrong was coarser: a whole file was never opened. A fourth round then found the--helptext still calling--mode accept-edits"the safer choice for pure write tasks" — the claim this release retracts in seven other places — plus a sentence incommands/delegate.mdspliced in half by an incomplete find/replace, and a comment justifying aset -efix by citing the wrongsetline (copied from doctor.sh, which has no-e; this file does). The accept-edits guard is LINE level for that reason: file level could not see it, because the same file retracts the claim three hundred lines further down. A fifth round foundagy-job.sh'src_label()— which mirrors these exact codes — still telling a background job to "pass --yolo (agy >= 1.1.3)". The guard added to prevent exactly that had enumerated the files it knew about and left this one out; both reviewers named the enumeration itself. It is a glob now, it triggers on the code as well as the phrase (this file writes a bare15)case arm and never the words "exit 15"), and it checks LINE level as well as file level — because the stale arm survived the file-level rule twice, once through the glob's omission and once because a comment two lines above mentioned 1.1.13 and satisfied the file. What all five had in common is narrower and checkable: the code named beside the old version alone.
- New:
/antigravity:migrate— move an existing Claude Code setup onto agy.agy plugin import claudealready exists, and on any current install it printsNo claude extensions found.and exits 0: it scans~/.claude/plugins/<name>/one level deep, and Claude Code 2.x keeps plugins atplugins/cache/<marketplace>/<plugin>/<version>/. It also does not follow symlinks. It does, however, run without authentication — soagy-migratecopies the real plugins flat into a stagingHOME, lets the native importer do the conversion there, repairs its output, and merges. Keeping Google's converter in the loop means the output tracks their format instead of ours. Two repairs are not optional. The importer turns a remote MCP server ({"type":"http","url":…}) into{"command":"","args":null}— the URL is discarded and noserverUrlwritten, leaving an entry that fails on PATH lookup. And it copies hooks byte-for-byte, though Antigravity'shooks.jsonis a map of named hooks, its matchers are step types (run_command, notBash), it fires five events rather than Claude's nine, and it never sets${CLAUDE_PLUGIN_ROOT}. - Skills are linked, not copied.
~/.gemini/config/skills.jsongets an entry pointing at~/.claude/skills, so both tools read one set of files. The path is written absolute:~/is not expanded in the global config, only in workspace ones. - Memory is rewritten, not stripped. A rule without
trigger: always_onin its frontmatter is ignored by Antigravity with no error and no warning — the failure mode a "strip the frontmatter" migration would produce is invisible. Global memory becomes aclaude-code-memoryplugin's rules (the only always-on channel that works with no project bound); per-repo memory goes to<repo>/.agents/rules/and registers the repo in~/.gemini/config/projects/, because workspace customizations do not load at all for an unbound session. Oversized notes are split on paragraph boundaries at the undocumented 12,000-character per-rule cap, and[[wikilinks]]resolve to relative links or flatten. - Permissions are proposed, never applied silently. Claude's
allowlist holds whole command lines — quoted prompts included — not prefixes, so nothing maps 1:1 onto agy'scommand(). Collapsing them always widens the grant, so the result goes to~/.gemini/.agy-migrate/proposed-permissions.jsonand needs--apply-permissions.model,effortLevelandenvare reported and deliberately not written. - Dry-run by default;
--applybacks up first and--uninstall --applyreverses it, including merged JSON keys. Re-running is a no-op, and a generated file whose marker comment you removed is treated as yours. The Claude Code config dir is never written to — the one exception is anAGENTS.mdsymlink beside aCLAUDE.md, under--include-repos. - Failure is propagated, not swallowed: a step that fails prints
✗, is named in a summary, and makes the run exit non-zero. The manifest is written after every step rather than once at the end, so a crash halfway through still leaves--uninstallable to find what was already written. bin/agy-migrateexits 16 when python3 is missing — the next free code in the plugin's contract. 13 is "agy not on PATH" and 14 is "model unavailable"; reusing either would misreport which dependency is actually absent.--include-orphan-memorywrites flat intorules/with a filename prefix rather than arules/orphan-<id>/subdirectory. Whether Antigravity descends into a rules subdirectory was never measured, and shipping an unverified layout is the exact silent no-op this tool exists to prevent.- The scan no longer walks
$HOMEfor.mcp.json. Running the migration on a real machine imported 40 MCP servers the user had never configured — vendored plugin catalogues found in two separate caches (~/.claude/plugins/marketplaces/and Claude Desktop'slocal-agent-mode-sessions/.../rpm/plugin_*/). Blocklisting caches is a losing game, so discovery now consults only the project roots Claude Code itself records, plus the desktop config.CLAUDE.mdstill walks, with both config trees,~/Libraryand dot-directories pruned. - A misspelt
--only/--skipunit is a usage error instead of an empty plan that reads as "nothing to migrate" — the same silent no-op class the rest of this entry is about. --rootsnarrows MCP discovery, not onlyCLAUDE.md. It previously folded in every project recorded in~/.claude.jsonregardless of the flag, so it could only ever broaden the scan — which is not what the help text or the skill's flag table said.- Exclusion compares on a path boundary, not a bare string prefix.
~/.claude-pro— a second Claude Code profile selected withCLAUDE_CONFIG_DIR— shares a prefix with the excluded~/.claudeand was being pruned from the scan, silently, as was anything like~/Library-notes. - An MCP entry referencing
${CLAUDE_PLUGIN_ROOT}is reported, not imported. Antigravity never sets it, so the server could only ever fail to start. - Registering an agy project is not enough, and the tool no longer implies it is.
agy -palways uses the id inantigravity-cli/cache/default_project_id.txtregardless of cwd, so per-repo.agents/rules/stays inert in headless runs. The report now printsagy --project <id>for each repo. (The written project shape is confirmed correct: passing that id back does load the workspace's rules.) - An encoded project directory that maps to two different real paths (
a_banda/bboth encode toa-b) resolves to neither. Picking by dict order could file one repo's memory into another repo's.agents/rules/. --uninstallremoves our entries fromskills.jsonandimport_manifest.json, not the files. Both are shared with agy and the desktop apps, which append to them after a migration; deleting the file wholesale — which is what "we created it, so we can remove it" amounted to — would have taken those additions with it. The file is deleted only if nothing remains once our keys are gone.- Exit codes conform to the plugin's shared table rather than carving out exceptions:
a bad flag exits 1 (argparse's own 2 would have read as "agy failed"), a missing
prerequisite exits 18, and a failed run exits 17. All three are in
docs/TROUBLESHOOTING.md. - Session history is not migrated and cannot be: Antigravity stores conversations as
opaque protobuf blobs inside per-conversation SQLite files.
docs/MIGRATION.mdhas the full layout reference and compatibility matrix.
doctornow asks agy which model it will run, instead of inferring it from a version string. 0.22.4 added a warning for agy below 1.1.10, where--modelwas ignored in headless-p. That warning is the best a version comparison can do, and a version comparison is a proxy: it is right about the releases we know about and silent about every other way the flag can fail to land. agy 1.1.11 answers the read-only slash commands in print mode without starting an agent turn, sodoctorrequests a tier model with-p /modeland reports which one comes back —usage.total_tokens: 0, no quota spent, no conversation left behind. It reads the tab-separated reply's slug and matches it against a tier configured as a display name, the same either-direction comparisonagy modelsneeded in 0.20.x. Gated at 1.1.11 and tested as a fact, not as prose. Below that version the slash command is not recognised, falls through as literal prompt text, and the model answers as though it had run — so probing there would spend a real turn and then trust the answer it invented. The stub agy in the suite logs every invocation, so "never probes below 1.1.11" is asserted against the log. An empty answer draws no conclusion in either direction: an older build than the version claims, a hang, or a plan that refuses the probe is not evidence that routing is broken.doctorvalidatespermissions.allowentries. The plugin recommends such a rule in eight places as the narrow alternative to--yolo, and the recommendation ships a placeholder —write_file(<dir>). A rule agy cannot parse announces itself in neither direction, and which way it fails depends on the version: from 1.1.11 it matches nothing, so the grant is absent and the write is soft-denied (exit 15) with the rule sitting visibly in the file; before 1.1.11 an entry that tokenized to zero command words matched every command and silently auto-approved anything the agent ran — broader than the--yoloit was chosen instead of.doctorflags the entry, names the reason, and scopes the consequence twice: to your agy version, and to the reason. Every class means the grant is absent; only acommand(...)rule naming no command also carries the match-everything history, so that sentence is printed only when such an entry is actually present. A mistypedwrite_file()never had it, and putting a security claim in front of someone holding one would be worse than saying less. (Both reviewers caught the first cut attaching it to every finding.) The zero-command-word test follows upstream's own examples (command(time)— a shell reserved word that prefixes a command without being one — a comment-only entry, and an empty compound()), plus the unsubstituted<...>placeholder, which is ours — matched as that shape, not as a bare angle bracket, so a rule carrying a literal redirect (command(echo hi > /tmp/f)) is not misread as a template nobody filled in. Rules it cannot judge are left alone: unbalanced quotes are agy's parser's business, andwrite_file(...)is a different matcher fromcommand(...). A false positive here sends someone to edit a rule that was always fine, so the well-formed case is pinned as hard as the broken ones.- Verified against agy 1.1.11, no change needed: all three tiers still resolve
(
flash→gemini-3.5-flash-high,flash-lo→-low,pro→gemini-3.1-pro-high); the exit-14 classifier still fires on an unknown model; and 1.1.11's reworded model-loading errors do not disturb the exit-15 path, whose message still containsauto-deniedandpermissions.allowverbatim — two independent anchors the classifier already matches./usageand/quotawere considered fordoctorand rejected on evidence: they return zero bytes here, which is not a bug but this account type — 1.1.11's own notes say credits do not apply to accounts signed in through a Google Cloud project or ADC./model,/effortand/skillsall return data on the same setup, which is what made the probe above possible. - The suite had a false-negative construct in 25 assertions, and this release woke it
up.
printf '%s' "$x" | grep -q PATis the shape fixed in 0.21.1 and explained in a comment in this very file:grep -qexits at the first match and closes the pipe, the writer dies of SIGPIPE (141), andset -o pipefailmarks the whole pipeline failed — so the assertion reads "not found" while the text is right there. The window is whatever the writer still has to emit after the matched line, which is why it survives review and why adding output below a match can revive it: the 1.1.9 version-gate assertion started failing 1–4 times in 8 concurrent runs on this branch and never on master. Diagnosed rather than guessed. Instrumenting the test made it vanish (it changes the timing), so the trace went insidedoctorinstead:mktempnever failed andAGY_VERwas never empty, which left only the assertion misreading output that had in fact been printed. All 25 now use acase-basedhas()— same test, no second process, no pipe — and the fivesed … | grep -qsource scans givegrepa process substitution instead, sosed's death is no longerpipefail's business. Those five failed silently: a false negative there reads as "the property holds", and among them are the guards for the delegate wrapper's--helpprobe,agy_guard's pipe rule, and thesort -Vdependency. 24 consecutive clean runs at the concurrency that reproduced it; each converted scan re-checked by mutation. That fix then shipped the same defect in a new shape, and both reviewers caught it.has()was defined beside the doctor tests, above which two call sites already sat — and bash does not hoist, so those two werecommand not found, exit 127,elsetaken unconditionally,okprinted regardless. One of them was the "json envelope leaked to stdout" assertion, which had already been voided once before by a different accident. Helpers now live at the top of the file, andtests/check-helper-order.pyruns first and fails the suite if any function is called above its definition. bash 4'scommand_not_found_handlewas tried for this and removed: macOS ships bash 3.2, where merely defining it is a silent no-op — a guard that reads as protection and provides none, which is the defect this whole entry is about. The static check works on any shell and was verified by putting the original bug back: it names the call site and the definition line. Its first regex missedelif, a call inside acasebranch, and a brace group — all three confirmed against the old pattern — so it now splits the line into command segments and compares each segment's first word, with no list of contexts to keep complete. The checker has its own tests, including a no-false-positive case, because a guard that misses a shape is the defect it exists to prevent. It also flagged its own test data, which is fair: a fixture written across real lines is not distinguishable from code, so the fixtures are single-line now. - The exit-15 message now points at the rule itself. It is the one place someone
actually lands when a write is soft-denied, and it recommended
permissions.allowwithout allowing that the rule is the problem: "if a rule is ALREADY in that file and you are still reading this, suspect the rule: runagy-doctor". Same caveat added toagents/antigravity-delegate.mdandcommands/delegate.md, the two operational files consulted while building a delegation call. The pre-run write nudge got the short half only —<dir>is a placeholder,agy-doctorwill say whether yours parses — because that one fires on every write-looking task, while the diagnosis belongs where the failure is. And the same overclaim the code fix removed was still sitting in the prose it was copied from. Five documents put the placeholder caveat next to the pre-1.1.11 match-everything history, which reads as though a mistypedwrite_file(<dir>)carried it — the exact conflationbad_allow_rulesclassifies apart (unparseable, notzerowords). Fixing the output and leaving the sentence that produced it just relocates the error, so all five now say the placeholder grants nothing on any version and namecommand(...)explicitly for the part that is version-sensitive. - The remaining 24 piped assertions are converted too, because the direction that was
left is the dangerous one. Review flagged them as a follow-up on the grounds that they
are positive matches, where a SIGPIPE produces a noisy false FAIL. A scan says the
opposite: 17 of them are negative assertions, and SIGPIPE is only possible when
grep -qfinds a match and exits early — which for a negative assertion is exactly the moment it is supposed to fail. They cannot flake in CI, because they only break when they have a real bug to report. Each becomesgrep … <<<"$x": a here-string has no writer process to kill and the pipeline is one command, sopipefailhas nothing to poison, and-F/-i/-E/ BRE semantics survive untouched — unlike a rewrite tocase. Verified by mutation on both the plain and the-qEalternation forms. The first pass at this shipped a regex that truncated a grep pattern at a)inside it, producing valid shell that searched for the wrong string and hung a polling loop. Caught because the suite stopped completing, not because anything reported it. - A quote in a comment disabled the whole allow-rule validator, and only the positive
tests noticed. A single quote inside
python3 -c '...'closes the shell string; bash parses the rest as arguments and redirections, which stays valid shell, sobash -nand shellcheck both pass, python runs a truncated program, and — with stderr on/dev/nullas these blocks have — the caller reads "nothing to report". Every negative allow-rule test still passed.tests/check-embedded-python.pynow runs overscripts/andhooks/and fails the suite on either signature of a truncation: a body ending on a comment line (where an apostrophe in prose lands) or one that no longer compiles (where an apostrophe in code lands). Both verified by mutation. The first attempt looked for the closing quote at the start of a line and false-positived onhooks/nudge-delegation.sh, where it sits at the end of one — the shell string ends at the FIRST quote, and that is the only part of this that is unambiguous. - Three claims in this entry now have tests behind them.
command(), a bare()and a comment-only rule were named as the zero-command-word examples and exercised by nothing;write_file()was described as not carrying that history and likewise untested. All four are pinned now, along with a partly-substituted path (write_file(/repos/<name>), flagged) and an angle-bracketed literal inside a command rule (command(grep -F <TAG> file.txt), left alone). That last one narrowed the placeholder test: shape alone is not enough, so it is applied only outsidecommand(...), where shell syntax lives and where the placeholder is never recommended. The cost is a placeholder inside a command rule going unflagged — a miss, which this file prefers to a false positive that sends someone to edit a working rule. - The report is tab-separated and a rule is user-supplied JSON, so an entry containing a tab shifted every field after it — and the field that moves is the class, which decides whether the security consequence prints at all. A newline was worse: it split one finding across two lines, and the reader dropped the orphan while the count still counted it, so the header promised more entries than it named. Class goes first now and the entry is escaped. The two orderings fail differently, and one assertion cannot see both: with the entry last the orphan line has no rule text, the reader drops it, and only the count is wrong; with the entry first the orphan keeps rule text and prints as a finding with no reason. An earlier pass here dropped the empty-reason check after mutating only the escaping and concluding it pinned nothing — reverting the ordering then went unnoticed, which both reviewers caught. Both assertions are in, each verified against the mutation the other misses.
- And the exit-15 message, a
.sh, was missed by the sweep that fixed the same overclaim in five.mdfiles. It told anyone who reached it that "an entry agy cannot parse grants nothing (and before agy 1.1.11 granted everything)" — handing the command-rule history to thewrite_file(<dir>)placeholder it had named two sentences earlier. Now scoped, and pinned by a test, which is what the five documents still lack.
--tierdid nothing on agy below 1.1.10, and nothing said so. agy 1.1.10 fixed--modeland--effortbeing ignored in headless-p— the flag was applied after model configuration had initialised, so the run silently fell back to the persisted default. This wrapper resolves every--tier(and everytier_*remap) to--modeland always runs-p, so on an older agy the entire routing story was inert while looking like it worked: the call succeeds, returns sensible text, reports usage. Only a bogus model surfaced anything, because validation still ran — which is why--model no-such-modelcorrectly returned exit 14 all along and hid the rest.doctornow warns on agy < 1.1.10, with the version compare done properly: a string compare puts 1.1.10 below 1.1.9, and the boundary is the whole point of the check.- Retracted: the 3.5 / 3.6 /
flash-mediumtoken comparisons from 0.22.0. Two independent reasons, either sufficient. Those runs were made on agy 1.1.8–1.1.9, so--modelwas being ignored and every arm may have executed the same persisted default. And the numbers did not survive their own ranges: 3.5-high spanned [421k, 509k] input against 3.6-high's [305k, 412k] at n=2 — separated by 2% — whileflash-mediumoverlappedhighoutright, and "−31% input" was a mean-vs-mean claim across those overlapping ranges.docs/POC-PLAYBOOK.mdtells you to report ranges rather than means for exactly this reason; this repo published the mean anyway. What stands: Gemini 3.6 Flash's output rate ($7.50/M vs 3.5's $9.00/M), which came from checking two pricing sources, not from those runs. Theflashdefault is unchanged. - Version-gate tests cover 1.1.9 (warns), 1.1.10 (must not warn — the boundary), a later minor, and an unparseable version, which is left alone rather than warned about.
- The bash gate now says why it blocked (#51,
reported by @potch8228 with a six-case repro table that reproduced exactly).
hooks/validate-delegate-bash.shrejects an unquoted newline — correctly, since a bare newline separates commands in bash — butBLOCK_MSGwas one fixed string for every rejection, and the header comment listed the metacharacters it rejects without mentioning newline. So a command refused for a stray trailing\nreturned character-for-character the same message as one refused for not beingagy-delegateat all. A caller could not tell "harmless formatting" from "you tried to run something else", and retried the same shape. The gate knew the reason at the moment it decided and threw it away. It now prints it to stderr — the pathBLOCK_MSGalready uses, which Claude Code feeds back to the agent — naming the newline and the remedy, the specific metacharacter, command substitution (including inside double quotes), an unterminated quote, a wrongargv[0], too many pipes, or a non-allowlisted pipeline producer. The reason never quotes ANY of the command back — not evenargv[0]. It lands in the agent's context and a blocked command routinely carries a delegation prompt; a character name and an offset are enough.argv[0]looks like a safe exception and is not:head()returnsshlex.split(seg)[0], the first shell word, so"some prompt text" agy-delegate ...makes attacker-chosen contentargv[0]. Restricting it to name-shaped tokens does not help either — an API key is name-shaped. Caught by both PR reviewers, whose finding also exposed that the test written to cover it could not fail: it placed the marker after a validargv[0]and behind a;, so the scan rejected the command first and the branch under test never ran. Five replacement cases now exercise the branches directly. - Leading and trailing whitespace is stripped before scanning. Line 45 already computed
cmd.strip()to test for emptiness and discarded it. bash ignores surrounding whitespace, so this cannot change what a command does, and a newline with nothing after it cannot begin a second one — it is the case you hit whenever a command is composed programmatically. Internal newlines are untouched and still blocked:agy-delegate\n "hi"is genuinely two commands, so the reporter's case 6 does not flip, and neither does a newline after an unquoted pipe. Stripping also cannot rescue an unterminated quote. Not taken: allowing a newline immediately after an unquoted|. Safe in isolation, but it means editing the scanner's state machine rather than normalising before it — a different risk class on the one file that is the only restriction on what the delegate subagent may run, and change one already tells the caller how to fix it. - Verified the block set did not move: 22 representative payloads produce identical verdicts before and after, and only the three intended whitespace cases flip. 23 new tests (191 → 214): 11 fail against the gate as it was, 5 fail against this change's own first attempt, and the rest pin behaviour that must not regress — an internal newline, a newline after an unquoted pipe, an unbalanced quote, and an escaped backslash followed by a bare newline (which is not a line continuation, and which the first version of that test got wrong).
- Correction:
--yolois not the only way to grant a headless write, and we said it was in eight places. Awrite_file(<dir>)entry underpermissions.allowin~/.gemini/antigravity-cli/settings.jsongrants writes recursively beneath<dir>with no flag at all. Confirmed on agy 1.1.9 by @rickberguer with a controlled A/B (#37): a covered target wrote, an uncovered one returnedPERMISSION_DENIED, the rule the only variable. agy's own soft-deny message names the rule and offers--yoloas the alternative — the CLI had been saying this for a while and we had not. This matters beyond accuracy: we were recommending--dangerously-skip-permissions, which approves every tool, where a grant scoped to one directory subtree would do.--yolois still what you need when no rule covers the target, and for web / Vertex AI Search / terminal /define_subagent— awrite_filerule covers writes only. The eighth place was the wrapper itself. The write-task nudge fired immediately before a write that then succeeded, telling the user headless agy "will NOT write to your workspace without it". Corrected, along with the exit-15 message — which is exactly where someone lands after a denial and so is the best place to name the narrower fix. Also README, SKILL.md, POC-PLAYBOOK.md, TROUBLESHOOTING.md (both the fix list and the exit-code table),commands/delegate.mdand the delegate subagent's own instructions. Scoped to what was actually measured: not verified below agy 1.1.9, and a glob form (write_file(/path/**)) was reported not to match. The wrapper cannot seesettings.json, so the nudge stays a warning rather than a check — it just no longer asserts something false. - Tests assert the wrapper offers the
permissions.allowroute on both the warning and the exit-15 path, and that it no longer claims--yolois required. The warning assertion now matches a stable substring — that string has been reworded twice and an exact-phrase test breaks on prose edits rather than on behaviour. - Confirmed: the #37 hang fix clears the reporting environment. 16 MCP servers (9 stdio,
7 remote):
agy-doctor3.5s all-pass and a delegation round-trip in 6.7s, both previously hanging. The remote servers are spelledserverUrl, matching what 0.22.1's detector assumes.
- Fix: every wrapper hung forever when stdio MCP servers were configured. Reported by
@rickberguer (#37) on macOS with a healthy, authenticated agy.
agy's stdio MCP children inherit its stdout and outlive it, so they hold the write end of a command-substitution pipe open andOUT="$(agy ...)"never sees EOF. The wall-clock guard cannot help: it killsagy, not the grandchildren — which is why this presented as an unbounded hang despite the timeout that exists for exactly this class of problem. Isolated cleanly by the reporter: same machine, onlymcp_config.jsonchanged — 16 servers hung on a pipe and returned in 6.5s to a file; 0 servers returned in 5.6s either way. Blast radius was everything.doctorhung insideagy_guard, so/antigravity:setupreported a broken or unauthenticated CLI while auth was fine, andagy-delegatehung on every call, takingagy-job,delegate,review,researchand the delegate subagent with it.agystdout now goes to a temp file, which children inherit harmlessly — the main call, and theagy --helpcapability probe, which had the same hazard and was not in the report (that line has now been wrong twice, for two unrelated reasons; see 0.21.1). Cleanup is folded into the existingEXITtrap, so it also happens on the timeout path where a trailingrm -fnever runs.doctor'sagy_guardredirects internally andcats at the end, socatis the only writer to the caller's pipe and it always exits.agy-job.shalready redirected to files and needed no change. Guarded by shape, not symptom — a hang cannot be asserted on cheaply and the next refactor is where it returns: tests reject any command-substitution capture ofagyin either script, plus a stub that reproduces the inherited-stdout hang and shows the file form returning. All verified to fail against the unfixed code. doctornow names stdio MCP servers whenagy modelstimes out. The precondition is invisible from the plugin's side and currently reads as an auth failure, which sends people off re-authenticating for nothing. Counts both sources agy documents — the global~/.gemini/config/mcp_config.jsonandplugins/<name>/mcp_config.json— because a diagnostic that undercounts fails silently, for exactly the person it exists for. Remote servers are identified byserverUrlon agy 1.1.9, not theurl/httpUrlother MCP clients use. The hint describes agy blocking internally on a server that never finishes connecting — which reproduces with stdout on a file and is what can still hang here — not the pipe mechanism this release removes.
- Docs: the number of delegations is the lever — batch them. Benchmarking this
plugin (Opus 5 conductor · Gemini 3.6 Flash High executor · agy 1.1.8 · n=3/arm, cold
cache) confirmed the per-delegation economics and located what actually breaks them.
Offloading a large corpus worked as designed — the conductor's
cache_readfell 61%, it never opened the corpus itself, each digest came back at ~4k tokens — but eachagy-delegatecall is an independent session sharing no cache with the last, so a conductor that delegated 7.3× against the same corpus paid to ingest it 7.3×. Two-thirds of the executor's cost was re-reading material it had already read; break-even was ~5.7 delegations. Correction to shipped guidance: rule 6 told you to keep an agy session alive with--continueso the working context "lives on the cheap side". Measured, that is backwards — resuming carries the whole prior conversation forward and agy re-reads the material anyway, so the continued call cost +82% / +277% vs a fresh one (n=2), withcache_read3–14× higher.--continueis for resuming after a quota/timeout failure, not a cost lever. The only lever that actually removes a re-ingestion is folding related units into one fully-specified delegation (rule 4). Also recorded: delegation moves work rather than removing it (~2.8× the normalized token volume for the same result), and agy's own prompt cache covers only ~2/3 of its context re-reads — both push toward fewer, larger delegations. Stated as direction from one configuration, not as constants. AGY_USAGE_LOG— a side channel the conductor's own habits can't truncate.AGY_USAGE/AGY_SIGNALgo to stderr, but this skill tells the conductor to keep its context lean, so it writesagy-delegate ... 2>&1 | tail -N; stdout (the digest) is emitted after the usage line, sotailkeeps the digest and drops the usage. Measured in the wild: a benchmark harness lost most of its Gemini-side cost data this way, making the hybrid look cheaper than it was. SetAGY_USAGE_LOG=/path(or theusage_logoption) and both line types are appended to that file as well. Appends (never truncates), off by default, and an unwritable path is non-fatal — measurement must not break the work.agy-tracenow covers plain delegations, not just internal-fan-out subagents. Every agy run leaves a readabletranscript.jsonl, andagy-delegateprints theconversationIdinAGY_USAGE, so cost and trajectory join 1:1 (verified 10/10 in the benchmark). New--audit(step-type counts + every non-zero exit) and--last. This makes the skill's non-negotiable "never trust agy's self-reported GREEN" rule actually checkable: measured, a delegation reported SUCCESS while 6 commands inside it failed. Documented limit: the command strings are recorded nowhere (not intranscript.jsonl,transcript_full.jsonl, orcli-*.log) — you get that a command ran, its exit code and its output; to attribute a filesystem change, diff the tree.- Fix: structured-error classification broke on any error containing quotes. agy
quotes the offending value in its message (
invalid model selection (--model \"X\" ...): model X is not recognized as a known model), and the wrapper pulled theerrorfield out withsed 's/.*"error": *"\([^"]*\)".*/\1/'— which stops at that first escaped quote, discarding the diagnostic phrase that follows it. The classifier therefore never saw it: a bad--modelortier_*remap reported a generic "agy failed" (exit 2) instead of MODEL_UNAVAILABLE (exit 14) with the "runagy models" hint. Present since 0.21.0, i.e. the structured-output release existed to stop exactly this kind of misclassification. python now writes the raw error to its own file instead of it being re-parsed with sed. The old stub's error string had no embedded quotes, which is why the suite stayed green while this shipped — the new stub uses agy's real wording. prices.json: recorded Gemini 3.6 Flash's rates without repricing the shipped default. The VM confirmed 3.6 Flash output at 7.50/M (vs 3.5's 9.00; in and cached-in unchanged) — butagy-cost-compare.shpicks thegemini_flashkey by tier name, andmodel_for_tier()'sflashtier still resolves to Gemini 3.5 Flash (High). Sogemini_flashstays at 9.00, which is correct for what ships, and 3.6's rates live in a newgemini_flash_36for anyone who remapstier_flash. (An earlier commit in this branch changedgemini_flash.outto 7.50 and asserted 3.6 was the default — contradicting this same PR's SKILL.md text, and understating Gemini output by 17% out of the box. Caught in review.) Also addedcached_in(Gemini prices cached input at a flat rate — notcache_read_mult × in, which is Claude-deck only) and a note that Gemini's context-cache storage is time-billed and unreported by agy, so figures computed here are a lower bound.cached_inhas no consumer yet:measure-session.pyprices the orchestrator deck only. Two tests now hold this together: every hardcoded fallback inagy-cost-compare.shmust matchprices.json, andgemini_flashmust match whatever theflashtier actually resolves to.- Docs: Gemini 3.6 Flash High measured against 3.5.
⚠️ Retracted in 0.22.4 — these runs were on agy 1.1.8–1.1.9, where--modelwas ignored in headless-p, and the ranges overlapped. −23% input tokens for the same task (n=2, order-reversed) and output at $7.50/M vs $9.00/M — but it does not reducecache_read(+6%) and is ~29% slower.flash-mediumis −31% input / −21% wall butcache_read+43% (n=3, reproduced), so it loses on cache_read-dominated agentic work. Theflashdefault stays on 3.5 for plan availability; remaptier_flashtogemini-3.6-flash-highwhen your plan serves it.
- Fix: the JSON-mode capability probe could silently disable structured output
(SIGPIPE race).
agy-delegate.shprobed support withagy --help 2>&1 | grep -q -- '--output-format'.grep -qexits at the first match and closes the pipe, soagy --helpcan die of SIGPIPE (141); underset -o pipefailthe whole pipeline reads as failed, JSON mode stays off, and noAGY_USAGEline is emitted — which is indistinguishable from "no delegation happened". Measured on a loaded container during a benchmark run: ~75% of calls lost their usage line (2/25 locally under no load). The probe now capturesagy --helponce into a variable and matches with a shell glob — no pipe, nogrep, no race. Regression tests assert both the absence of the pipe and 20 stable probes. (Found by @yuting0624's benchmark harness — exactly the "silent success" failure class this plugin exists to catch elsewhere.) - Docs: corrected the
AGY_USAGEaccounting semantics.total = input + output(thinkingis insideoutput), andcache_readis a separate counter — not part oftotal, and not a subset ofinput: in an agentic delegation it can far exceedinput(measured 1,356,694 vs 243,117). Price the Gemini side as three separate terms. This is the opposite convention from the Claude/Harbor side, where cache-read tokens are an inner subset of the input total. An earlier note in 0.21.0 stated the subset reading; that was over-concluded from a sample wherecache_read < input.
- Structured output (agy 1.1.8): reliable failure classification + real executor token
usage. agy 1.1.8 shipped
--output-format json— the thing we'd tracked as "not externally available" since 0.13. The wrapper now uses it internally, and the stdout contract is unchanged (callers still get the model's text):- Failures are classified from the structured
status/errorinstead of pattern-matching prose. This matters: agy's stderr wording has shifted repeatedly (the same failure surfaced as both "invalid argument" and a hang), and in JSON mode stderr is empty — the diagnostic moves into the envelope. The wrapper reads the envelope first and still falls back to stderr patterns. - New
AGY_USAGE {...}line on stderr with the executor's real token accounting —input/output/thinking/cache_read/total+conversation_id. The Gemini side of a delegation can now be measured, not estimated (stderr, so it never pollutes the conductor's context). - Gated and reversible: only when agy advertises
--output-format,python3is present, and the newstructured_outputoption isn'toff— otherwise the dependency-free plain-text path runs unchanged. Tests cover both paths. - Verified live on agy 1.1.8 (success, model-unavailable → exit 14, opt-out), plus stubs for quota/fallback.
- Failures are classified from the structured
- Upstream caveat found while implementing: agy 1.1.8 emits a raw newline inside the
responsestring, so the payload is rejected by strict JSON parsers. The wrapper parses leniently; worth reporting upstream. - Docs corrected everywhere they claimed
--output-format jsondoesn't exist (README, SKILL) — that statement is no longer true as of agy 1.1.8.
- New: multimodal delegation —
/antigravity:media(agy-media). Claude Code can't hear audio or watch video, and doing it locally means an ffmpeg + speech-model stack. Gemini is natively multimodal, so this delegates the perception to agy — no local transcription stack required. Verified end to end on agy 1.1.7 (audio transcribed; video analyzed with per-scene visuals + OCR + speech).- Cost discipline built in (the differentiator): agy writes the full timestamped
transcript to a file and returns only a compact digest (summary · timestamped
outline · key points · quotes with
[mm:ss]· action items · visuals · uncertainty notes). A 1-hour recording is ~10k words — exactly thecache_readblow-up the plugin exists to avoid, so it never lands in the conductor's context; Claude reads slices of the transcript on demand to verify. - Format pre-flight: agy's media handling is narrower than the Gemini API. Verified
working:
wav mp3 flac ogg opus | mp4 mov webm | png jpg webp..m4a/.aifffail (inconsistently — "invalid argument" or a hang) despite Gemini itself accepting them — an agy-side gap. Rather than surfacing that cryptic failure, the engine checks the extension first, exits5with the exact conversion one-liner, and--convertdoes it for you (macOSafconvert, elseffmpeg). - Verification framing: the digest must flag inaudible passages and uncertain names/numbers; the command tells Claude to treat those as unverified and check the transcript slice before relying on them. Warns if agy returns a digest without actually writing the transcript file.
- Long media: default
--timeout 15m, size heads-up over 25 MB, and guidance to split (~30-min chunks) if it still times out.
- Cost discipline built in (the differentiator): agy writes the full timestamped
transcript to a file and returns only a compact digest (summary · timestamped
outline · key points · quotes with
- Security: harden the delegate subagent's Bash gate against command-injection bypass
(#29, reported by
@ktseo41).
hooks/validate-delegate-bash.shis the only restriction on what theantigravity-delegatesubagent can run via Bash; it matched the wrapper name as a substring anywhere in the command, so payloads like... # agy-delegateorecho $(...) agy-jobwere approved — arbitrary command execution under prompt injection. The gate now:- requires the first command token (argv[0], basename, optional
.sh) to be exactlyagy-delegate/agy-job— a token check, not a substring match; - allows only one pipeline shape,
<git|cat|echo|printf> | agy-delegate|agy-job -(sogit diff | agy-delegate -keeps working); - rejects unquoted
;&|<>()#, backticks, and$((command substitution) — while permitting those characters inside a quoted prompt (no false positives on legitimate prompts;$(...)/backticks inside double quotes are still blocked because bash would expand them); - fails closed (block) if the JSON is unparseable or
python3is unavailable (the old fallback matched against the raw JSON, which was fail-open-ish). - Regression tests cover both the bypass vectors and legitimate-usage false positives.
- requires the first command token (argv[0], basename, optional
- Added
SECURITY.mdwith a private vulnerability-reporting channel (the reporter noted its absence). Report via GitHub Security advisories.
bin/measure-sessionshim —measure-session.pywas the only script without abin/entrypoint, so it couldn't be run by bare name from a marketplace install (only via thescripts/path, which doesn't resolve from a user's own repo). It now has a shim like the others, someasure-session <session-id>works on the PATH. (doctor+ contract tests cover it.)- Fan-out recipe corrected for agy 1.1.3+. The skill's internal fan-out example now
leads with the preferred
define_subagent → invoke_subagentform and requires--yolo— on 1.1.3+ the subagent tools are soft-denied headless without it (the old "spawning needs no--yolo" note was 1.0.x behavior). Re-verified live on 1.1.5 (two parallel subagents, each with an auditabletranscript.jsonlviaagy-trace).
doctorfix — recognize tier models acrossagy modelsformat changes. agy 1.1.5 switchedagy modelsoutput from display names (Gemini 3.5 Flash (High)) to slugs (gemini-3.5-flash), which broke doctor's strictgrepand made it falsely warn that every tier model was missing (they still work). doctor now normalizes both sides (lowercase-alphanumeric, bidirectional substring), so it survives either format.- Verified against agy 1.1.4 / 1.1.5. Default
flashdelegation, tier resolution, and exit-code classification all work on 1.1.5 (1.1.4/1.1.5 were mostly interactive/UX:--effort, stable model slugs,/modelpicker, MCP fixes). - Gemini 3.6 Flash now appears in
agy modelsand works (verified all effort variants through the wrapper). Theflashdefault stays Gemini 3.5 Flash (High) for broad plan availability (newer models can lag on enterprise Vertex) — remaptier_flashtoGemini 3.6 Flash (High)if your plan serves it. (Both display names and 1.1.5 slugs are accepted by--model.) - Still unchanged upstream (re-confirmed on 1.1.5): headless writes need
--yolo(apermissions.allowwrite-rule was still soft-denied in testing despite 1.1.4's "honor settings.json headless"),--output-format jsonnot externally available yet, native Windows headless (#508/#6) unresolved.
- agy 1.1.3: headless write model changed again —
--yolois now the durable grant. All verified live on 1.1.3:- 1.1.3 removes the scratch-divert: a write/tool needing permission is now soft-denied in headless mode with a clear stderr notice (rc=0 + empty stdout). This is the upstream's intended behavior (announced), not a bug — the evolved issue #10.
--mode accept-editsno longer grants headless writes (soft-denied for create AND edit on 1.1.3 — it had been riding the auto-approve behavior that 1.1.3 closed). Docs, thedelegatecommand, and the write-task warning now point to--yoloas the reliable headless write/tool grant across versions;--modepassthrough stays but is no longer recommended for writes.- New structured failure
15— permission denied: the wrapper detects the soft-deny stderr (rc=0 + empty) and returns exit15+AGY_SIGNAL {PERMISSION_DENIED}with an actionable message ("add--yolo"), instead of a bare "empty output".agy-jobrenders it. - Note:
--output-format jsonis still not externally available, and native Windows headless (#508/#6) is still unresolved — WSL guidance and plain-text parsing stay.
- New structured failure
14— model unavailable (agy 1.1.2): agy now hard-fails (instead of silently downgrading to the default model) when--modelcan't be resolved. The wrapper classifies this into exit14+AGY_SIGNAL {MODEL_UNAVAILABLE}and prints an actionable hint (runagy models; fix--model/tier_*/default_model) — the common failure when a tier remap points at a model your plan doesn't expose.agy-jobrenders the new code. - Note on agy 1.1.x upstream fixes (verified against release notes): 1.1.1 fixed
agy -phanging inside a subprocess/script and print mode silently exiting success on a server-side error; both are now non-zero + stderr, so the wrapper classifies them correctly. Native Windows headless (#508/#6) is still not resolved upstream, and--output-format jsonis still not externally available — the WSL guidance and plain-text parsing stay.
- agy 1.1.0 support —
--mode accept-edits|planpassthrough (all behaviors below verified live on 1.1.0):- 1.1.0 makes review-first the default execution mode. Headless consequence
(measured): a write task without write permission no longer just "describes" —
agy writes the files to its own scratch dir (
~/.gemini/antigravity-cli/scratch) and reports success, while your workspace stays untouched (the evolved #10 failure mode). - New wrapper flag
--mode accept-edits: auto-applies FILE EDITS to the real workspace without granting terminal/tool permissions — a narrower grant than--yolo, now the recommended way to run pure write delegations.--yoloremains for tasks that also need tools (web / Vertex AI Search / terminal); verified backward-compatible on 1.1.0.--mode planpasses through for strategize-only runs. - The write-task warning now fires only when neither
--mode accept-editsnor--yolois set, and explains the scratch-divert behavior. - Subagents re-verified on 1.1.0 (
define_subagent→invoke_subagent, transcript path unchanged); they are officially documented as of 1.1.0, with static config at<workspace>/.agents/agents/*.mdand global~/.gemini/config/agents/— the skill's fan-out recipe now points at the official docs. delegatecommand, skill safety section, and README write guidance updated to the "prefer--mode accept-edits, escalate to--yoloonly for tools" split.
- 1.1.0 makes review-first the default execution mode. Headless consequence
(measured): a write task without write permission no longer just "describes" —
agy writes the files to its own scratch dir (
- Frictionless delegation — no slash command required, judgment stays with Claude.
Two additions reduce the "you must type
/antigravity:delegateevery time" friction, while deliberately NOT auto-routing (full automation below the break-even is a measured net loss):- Proactive subagent selection: the
antigravity-delegatedescription now tells Claude to use it proactively for bulk work (scaffolding / exhaustive tests / migrations / fan-out search) — with the explicit counterweight that the break-even judgment is Claude's, every time. - Prompt-level nudge (
hooks/nudge-delegation.sh, UserPromptSubmit): a cheap, deterministic heuristic (volume/fan-out phrases, EN + JA) adds a short advisory note when a prompt looks above the break-even. Advisory material only — the note itself says "THE JUDGMENT IS YOURS". Only thepromptfield is scanned (no cwd/path false positives), the user's prompt is never echoed back (no injection surface), and it stays silent when the user is already delegating. Toggle via the newdelegation_nudgeplugin option.
- Proactive subagent selection: the
- Internal fan-out recipe updated for agy 1.0.16 (re-verified per the recipe's own
"re-verify after upgrades" caveat — which became real within a day): dynamic custom
subagents now work —
define_subagenta named specialist in-session, theninvoke_subagentit by TypeName (1.0.13–1.0.15 shipped this broken, upstream #521; fixed in 1.0.16 via the JSON→Markdown definition change). Theself+Role pattern stays as the any-version fallback (re-verified on 1.0.16).transcript.jsonllocation is unchanged across 1.0.12→1.0.16, soagy-traceis unaffected. The recipe now flags the whole surface as fast-moving (4 upstream releases in a week; official static agent-config docs drift from behavior, upstream #527).
- Internal fan-out recipe +
agy-trace(community pointer to upstream antigravity-cli#105; verified headless on agy 1.0.12): agy'sinvoke_subagentsandbox only allows TypeNamesself/research— custom TypeNames are rejected. The skill now documents the role-delegation pattern (TypeNameself+ a specialistRole) for one-delegation internal fan-out, so coordination tokens land on the cheap (Gemini) side instead of the frontier side. Spawning needs no--yolo(writes inside the work still do).- Each spawned subagent leaves a readable step-by-step
transcript.jsonlunder~/.gemini/antigravity-cli/brain/<conversationId>/— unlike the opaque conversation.dbblobs. Newagy-trace(script + bin shim) pretty-prints one (agy-trace <conversationId>), lists recent ones (--list), or emits raw JSONL (--raw) so Claude can run a real trajectory audit on what subagents actually did. - Skill's trajectory-check gate updated accordingly;
doctorcovers the new script/shim.
- Each spawned subagent leaves a readable step-by-step
--digestflag + digest-size guard — the cost discipline's biggest lever ("ingest digests, not dumps") is now enforced in code, not just prose (#5):agy-delegate --digestappends a digest-only output contract to the prompt (compact bullets + a one-lineDIGEST:trailer; no full files / raw logs).- The wrapper now warns on stderr when a reply comes back dump-sized (default
threshold 8000 chars) so the conductor doesn't silently ingest a raw dump. New plugin
option
digest_warn_charstunes it (0disables). delegatecommand + skill updated to use--digestfor bulk reads and to not ingest a flagged dump.
- Support docs — one-round-trip diagnosis: every environment bug so far (#6, #10,
#11, #15) needed the same three facts, so they're now asked up front:
- Issue templates (
.github/ISSUE_TEMPLATE/): the bug form requiresagy-doctoroutput, OS/platform, and install method (marketplace vs--plugin-dir). docs/TROUBLESHOOTING.md: symptom-first fixes — Windows headless hang (and the "agy works when I type it" console explanation), WSL/mntslowness, silent no-write without--yolo, exit-code/AGY_SIGNALtable, tier remaps, updating.
- Issue templates (
- Injected routing policy no longer references
$CLAUDE_PLUGIN_ROOT(#15, fix by @Masterisk-F in #16): the SessionStartadditionalContextinhooks/policy-context.jsonstill told the model to run"$CLAUDE_PLUGIN_ROOT/scripts/agy-delegate.sh"— but that variable isn't exported to model-run Bash (same root cause as #11), so it expanded empty and the model had to rediscover thebin/entrypoint. Now uses the bareagy-delegatebin name. (The #11 bin/ migration updated commands/skill/agents but missed this injected string.)- This ships as a version bump so
/plugin marketplace updaterecognizes the fix — #16 landed onmasterwithout one, leaving installs on 0.15.0 unable to see it.
- This ships as a version bump so
- Regression guard widened: the contract test now also fails if any SessionStart
additionalContextreferences$CLAUDE_PLUGIN_ROOT(not just commands/skill markdown), so this class of bug can't recur in injected context. - Version-drift guard: the contract test now asserts
SKILL.md'sversion:matchesplugin.json— they had drifted (skill stuck at 0.14.0 while the plugin was 0.15.0); re-synced to 0.15.1.
- New command —
/antigravity:cloud-run-debug(Conductor/Executor demo): diagnose a failing Cloud Run service. agy (Gemini) does the bulk, cheap work — pullingseverity>=ERRORlogs viagcloud logging readand clustering them into a structured digest (error clusters / representative stack traces / time distribution / likely root-cause candidates) — and Claude ingests only that digest to infer the root cause and propose a fix. The lean handoff keeps Claude's context (and cost) down.- Read-only by default — diagnosis + proposal only.
--applyis the only write path, and it only ever lands the fix on a dedicated branch with the diff shown for a human to review/merge; nothing is deployed or merged automatically. - Narrow surface, generic engine: one user-facing command, but the engine
(
scripts/cloud-debug.sh, shimmed asbin/cloud-debug) takes--resource-type(defaultcloud_run_revision) so a future gke-/functions-debug can reuse it without a rewrite. The digest reusesagy-delegate.sh— no new delegation logic. - Safety: uses the existing
gcloudADC (never asks for tokens); a missingroles/logging.viewerexits with the exactadd-iam-policy-bindingfix. --applydirty-tree guard + project visibility: before branching,--applychecksgit status --shortand stops if the tree is dirty (so a user's uncommitted changes can't leak into the fix's diff/commit); and--projectis now a surfaced flag, with the command confirming the resolved project when it isn't passed (avoids reading the wrong GCP project).- Lean by construction: the log payload handed to agy is field-projected
(
--format='json(timestamp,severity,textPayload,jsonPayload,httpRequest.status)', dropping resource/insertId noise ~5-10x) and byte-capped before the handoff (CLOUD_DEBUG_MAX_BYTES, default 200000; the tail is clipped — byte-accurate across locales, so multibyte logs are bounded too — and agy is told the clipped JSON is partial/invalid) — so the "cheap / lean handoff" claim holds even on noisy services where--limitalone bounds entry count but not byte volume. doctorchecks the new script/shim;tests/stubgcloud+agyand cover the fetch→digest, default--since, read-only (no writes / no--applyin the engine), and permission-denied paths.
- Read-only by default — diagnosis + proposal only.
bin/entrypoints — fixes$CLAUDE_PLUGIN_ROOTfailures on marketplace installs (#11):$CLAUDE_PLUGIN_ROOTis only substituted in structured config (hooks/MCP/LSP) and is not exported to model-run Bash — so commands/skills that ran"$CLAUDE_PLUGIN_ROOT/scripts/…"expanded to an empty path and failed. Scripts are now invoked by bare name viabin/shims (Claude Code adds a plugin'sbin/to the Bash-tool PATH):agy-delegate/agy-job/agy-cost-compare/agy-doctor. Commands, the skill, and the delegate subagent were updated; the PreToolUse gate accepts the bin names;doctorchecks the shims. (scripts/is unchanged — the shims forward to it.)- Write-delegation guidance + guard
(#10): without
--yolo, headless agy only describes edits and returns a confident success while writing no files. Thedelegatecommand now makes--yoloexplicit for write tasks (on a branch), notes the harness may prompt for / block--dangerously-skip-permissions, and flags the ~2-min sync Bash limit (→ background job).agy-delegate.shnow warns when a write-looking prompt lacks--yolo. (The verification gate already caught the no-write.) - Thanks to @erszcz (#10) and @Masterisk-F (#11) for the reports.
- Windows headless hang fixed / diagnosed (#6):
on native Windows without a console (ConPTY), headless
agy -p/agy modelscould hard-hang with a 0-byte log when stdio is redirected.agy-delegate.sh: wraps agy in a wall-clock guard (GNUtimeout/gtimeout, with--kill-after) sized from--timeout+ head-room, so a hang now returns a structured TIMEOUT (exit 12) +AGY_SIGNALinstead of blocking forever. Warns on native Windows when notimeoutbinary is available.doctor.sh:agy models(and the version probe) are now time-bounded and distinguish a hang from an auth failure — it no longer tells you to re-authenticate when agy is actually hung headless (the misdiagnosis that cost the reporter hours). A genuine empty result still reports "not authenticated".- README: added a Platform-support note (macOS/Linux/WSL supported; native Windows not recommended for headless delegation) and a known-limit entry.
- tests: added a hang → wall-clock-guard → exit 12 case (skips cleanly without
timeout). - Thanks to @rokushikii for the detailed, reproducible report.
- Configurable executor model (agy is multi-model): tiers still default to Gemini, but
each is remappable to any
agy modelsentry (Claude/GPT on plans that expose them) viatier_flash/tier_flash_lo/tier_pro, plus adefault_model(exact name) option — allCLAUDE_PLUGIN_OPTION_*. Precedence:--model> explicit--tier>default_modeldefault tier. Keeps Gemini as the recommended default (a different/cheaper executor is what yields the cost + cross-model-verification benefit).
- doctor: tier-model check now respects the remaps and warns instead of failing when a
model isn't in
agy models(agy is plan-dependent), with a remap hint. - (Reported via Reddit: agy supports Claude/GPT on non-Vertex plans.)
- WSL slow-mount guard:
agy-delegate.shwarns when--add-dirtargets a Windows mount (/mnt/*) under WSL — agy reads it over a slow 9p bridge, so even trivial calls can take 20s+ — anddoctorflags a workspace on/mnt/*. Fix: keep the repo on the WSL Linux filesystem (~). Also documented in known-limits. (Reported via Reddit.)
- Auto-injected routing policy (
hooks/): aSessionStarthook injects the plugin's cost-aware routing policy as session context (delegate above the break-even, keep Claude's context lean, always verify) so the discipline applies without invoking the skill. Toggle via thecoding_policyplugin option. A second hook does a fastagypresence/auth check on session start. - Delegation subagent (
agents/antigravity-delegate.md):tools: Bash, Read, Globwith aPreToolUsegate (hooks/validate-delegate-bash.sh) that permits only the delegation wrapper — noWrite/Edit, no arbitrary Bash — so file writing runs on agy/Gemini (no Claude tokens spent generating file contents); it returns a digest for Claude to verify. - Structured exit codes + signal:
agy-delegate.shnow classifies failures into10quota ·11auth ·12timeout ·13agy-missing and prints a machine-readableAGY_SIGNAL {...}line;agy-job.shsurfaces the code/label/signal instatus/result. - Plugin options (
userConfig):default_tier,timeout,coding_policy— read by the wrapper/hook viaCLAUDE_PLUGIN_OPTION_*(explicit flags still override). /antigravity:researchcommand: surfaces the skill's Claude-orchestrated deep-research recipe — agy fans out grounded web search (compact digests), Claude verifies each load-bearing claim across ≥2 independent sources and synthesizes a cited report.--print-command(agy-delegate dry run): prints the resolvedagy …invocation without executing — for debugging/trust; works even without agy installed.- Plugin-contract test: asserts the manifests, that every hook/agent file reference resolves, command/skill/agent frontmatter is present, and hook scripts are executable — catches a broken reference before release.
- CI: shellcheck + JSON validation now also cover
hooks/.
- Pricing config (
prices.json): single source of current Vertex rates (Opus 4.8 5/25, Sonnet 4.6 3/15, Gemini 3.5 Flash 1.50/9, Gemini 3.1 Pro 2/12).measure-session.pynow prints an estimated USD figure;agy-cost-compare.shdefaults come from it (env still overrides; Gemini rate picked by tier). - doctor: validates each tier→model name still exists in
agy models(guards against agy renaming models across versions). - CHANGELOG.md added.
- CI (GitHub Actions): shellcheck + dependency-free test suite + JSON manifest validation on every push/PR.
- Background jobs (
scripts/agy-job.sh, codex-style):start/list/status/result/cancel, daemonized worker + per-job registry. Slash commands/antigravity:status|result|cancel. For interactive sessions; headless stays synchronous.
- Code-review fixes: mktemp+trap for stderr (was a fixed
/tmppath = concurrency race); friendly arg validation; content-anchoredusage();--yolopassthrough + div-by-zero guard in cost-compare;with open+ scope caveat + multi-match warning in measure-session. - Slash commands
/antigravity:delegate|review|setup;scripts/doctor.sh; dependency-freetests/run-tests.sh.
- Repackaged for public release: sanitized internal identifiers, genericized references,
MIT
LICENSE, disclaimer.
- Deep-research recipe; verification gates incl. agy tamper-detection; cost-discipline
section (break-even, lean context, digest, cache-TTL trap);
measure-session.py;docs/AB-RESULTS.md(measured A/B) anddocs/DEMO-KIT.md.
- Initial plugin:
agy-delegate.shwrapper,antigravityskill (SDLC model routing, conductor/orchestrator),agy-cost-compare.sh, marketplace + plugin manifests.