◉ANTIGRAVITY LABJP
Articles/AI Tools
⚙ AI Tools/2026-10-15Intermediate

A 50-Line Wrapper That Hands Japanese Proofreading to agy's Headless Mode

Wrap the Antigravity CLI (agy) headless mode in a small proofreading script: how to feed stdin, why exit code 0 can still mean an empty answer, the argument-length ceiling, and a cost estimate that accounts for the January 2027 price change.

Antigravity CLI38agy5headless6shell scriptcost estimate

After reading a write-up about letting a second model proofread Japanese text, I wanted the same thing in my own setup. The idea landed because the habits of a writer are hardest to see for the writer's own model.

Then I started writing the script that calls agy -p, and small questions piled up. Can I pipe text in? What does failure look like as an exit code? And what does one run cost? As an indie developer who will lean on a single script, I wanted those three settled first.

One honest note up front: what I verified myself is the shell side. I did not connect to a real agy. I used a stub agy that echoes its arguments and changes its exit code on demand, and tested the wrapper's branches one by one. Everything about agy itself comes only from the official Headless mode page.

What the docs promise about headless mode

Headless (print) mode runs one prompt, writes the response, and exits. These are the parts that matter for a wrapper:

  • Pass the prompt with -p, --print, or --prompt. The response goes to stdout; diagnostics go to stderr.
  • It uses cached credentials, so run an interactive agy session once first. Without a terminal and without auth, it exits with authentication required instead of hanging.
  • Choose a model with --model <slug>; list slugs with agy models. Headless mode does not silently fall back, and an unknown slug exits non-zero.
  • Tools that need approval are soft-denied: the run continues, the exit code stays 0, and stderr gets a notice.
  • --print-timeout caps the wait (default 5 minutes).

One thing is easy to miss: the page does not describe piping plain text into -p. --input-format stream-json does read stdin, but it is a different mechanism that takes one JSON object per line. So the wrapper reads stdin itself and passes the combined prompt as an argument.

The 50-line wrapper

The instruction asks for at most five findings, each as original sentence, suggested fix, and reason. If you let the reviewer rewrite everything, the original voice disappears.

#!/usr/bin/env bash
# 日本語の文章を agy の headless モードで別モデルに校閲させる小さなラッパー
set -euo pipefail
 
MODEL="${JA_REVIEW_MODEL:-gemini-3.8-flash-medium}"
LIMIT="${JA_REVIEW_TIMEOUT:-3m}"
 
if [ "$#" -gt 0 ]; then
  TEXT="$*"
elif [ ! -t 0 ]; then
  TEXT="$(cat)"
else
  echo "使い方: ja-review.sh '文章'  または  cat draft.md | ja-review.sh" >&2
  exit 64
fi
 
if [ -z "${TEXT//[[:space:]]/}" ]; then
  echo "入力が空です" >&2
  exit 64
fi
 
# 引数1個の上限(Linux は 128KB)に近づく前に止める
if [ "$(printf '%s' "$TEXT" | wc -c)" -gt 100000 ]; then
  echo "入力が長すぎます(100,000 バイト超)。章ごとに分けてください" >&2
  exit 65
fi
 
PROMPT="次の日本語を校閲してください。直す箇所を「元の文 → 直し案 → 理由」の3点で最大5件。事実の追加はしないでください。
 
${TEXT}"
 
ERR="$(mktemp)"
trap 'rm -f "$ERR"' EXIT
 
OUT="$(agy -p "$PROMPT" --model "$MODEL" --print-timeout "$LIMIT" 2>"$ERR")" || {
  echo "agy が失敗しました(終了コード $?)" >&2
  cat "$ERR" >&2
  exit 1
}
 
# 終了コード 0 でも、承認待ちで道具が soft-deny されると本文が空になりうる
if [ -z "${OUT//[[:space:]]/}" ]; then
  echo "応答が空です。stderr:" >&2
  cat "$ERR" >&2
  exit 3
fi
 
printf '%s\n' "$OUT"
[ -s "$ERR" ] && { echo "--- stderr ---" >&2; cat "$ERR" >&2; }
exit 0

The slug gemini-3.8-flash-medium comes from the --model example on the official page. Slugs differ by environment, so check agy models first.

./ja-review.sh "a sentence to proofread"
cat draft.md | ./ja-review.sh
JA_REVIEW_MODEL=another-slug ./ja-review.sh < draft.md

Exit code 0, empty answer

The check I spent the most time on is the one that tests whether OUT is empty.

According to the docs, an operation that cannot get approval is soft-denied and the run continues with exit code 0. So agy can look successful while stdout is empty. A script that only checks the exit code will quietly accept nothing.

Reproducing it with the stub gave exactly that combination: exit 0, empty stdout, a notice on stderr. The wrapper turns it into exit code 3 and shows stderr as is. I realized I had been trusting exit 0 too much when I saw that empty answer for the first time.

SituationStub behaviorWrapper exit code
Normalprints text, exits 00
Empty inputagy is not called64
Input over 100,000 bytesagy is not called65
agy exits non-zero (e.g. unknown slug)reason on stderr, exit 11
Exit 0 but empty stdoutsoft-deny notice on stderr3

A proofreading job needs neither file writes nor shell commands, so I deliberately did not widen permissions. Using --dangerously-skip-permissions to force it through would leave room for a proofread to turn into an edit of your draft.

"Argument list too long" on big drafts

Reading stdin and joining it into an argument has a weak spot. In my first version, a roughly 150,000-byte draft (about 50,000 Japanese characters) stopped right at launch:

agy failed (exit code 126)
./ja-review.sh: line 29: .../bin/agy: Argument list too long

A 90,000-byte draft went through, which matches the Linux limit of about 128 KB for a single argument. So the wrapper now refuses anything over 100,000 bytes with exit code 65, before calling agy. Split long drafts by chapter.

A Zenn write-up also reports that -p consumes the next token, so the -p= form is safer. My prompt always starts with a fixed Japanese phrase, so a draft that begins with - cannot be mistaken for a flag. Fixing the head of the prompt was partly for that reason.

Estimate the cost first

Proofreading runs often, so even a tiny unit price starts to matter. On the Gemini API pricing page (checked on 2026-10-15), standard pricing for gemini-3.8-flash is:

PeriodInput (per 1M tokens)Output (incl. thinking)
Current$0.75$3.75
From January 1, 2027$1.50$7.50

The page states that these prices double on January 1, 2027. Here is a small estimator:

"""校閲1回あたりの API 料金を見積もる。単価は 100万トークンあたりの USD。
2026-10-15 に公式の料金ページで確認した gemini-3.8-flash の標準料金。"""
import sys
 
PRICES = {
    "now":        {"input": 0.75, "output": 3.75},  # ~2026-12-31
    "2027-01-01": {"input": 1.50, "output": 7.50},  # 2027-01-01 以降
}
 
def cost(in_tok: int, out_tok: int, period: str) -> float:
    p = PRICES[period]
    return in_tok / 1_000_000 * p["input"] + out_tok / 1_000_000 * p["output"]
 
def main() -> None:
    in_tok = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
    out_tok = int(sys.argv[2]) if len(sys.argv) > 2 else 1200
    runs = int(sys.argv[3]) if len(sys.argv) > 3 else 100
    for period in PRICES:
        one = cost(in_tok, out_tok, period)
        out_share = (out_tok * PRICES[period]["output"]) / (one * 1_000_000)
        print(f"{period:>10}: 1回 ${one:.4f} / {runs}回 ${one*runs:.2f} (出力が占める割合 {out_share:.0%})")
 
if __name__ == "__main__":
    main()

With 3,000 input tokens, 1,200 output tokens, and 100 runs a month (my assumptions, not measurements), the result was:

       now: 1回 $0.0067 / 100回 $0.68 (出力が占める割合 67%)
2027-01-01: 1回 $0.0135 / 100回 $1.35 (出力が占める割合 67%)

The surprise: output, not input, drives the bill. Output is the smaller side (1,200 against 3,000), yet at five times the unit price it makes up 67% of the cost. Cutting output to 300 tokens drops one run to $0.0034. Capping the findings at five helped quality, and it helped the bill too.

This only applies to pay-as-you-go API keys. I could not confirm whether these prices apply when you use agy through a subscription, so decide which route you use before relying on the numbers.

Gotchas

  • Run an interactive agy once first, or you will get an authentication error.
  • An unknown slug exits non-zero; the wrapper surfaces the reason from stderr.
  • --disable-slash-commands is not mentioned on the official page I checked. Run agy --help before using it.
  • Slugs and prices move. The numbers here are as of 2026-10-15.

Next step

Exit code 0 is not proof of success; an empty answer counts as a failure. I plan to carry that rule into my other scripts as well.

Pick one slug from your own agy models, run ja-review.sh on a single sentence, and if an empty answer shows up, the stderr notice is where I would start.

Sources

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⚙ AI Tools2026-09-15
The Day I Stopped Splitting by Skill — Three Questions for Routing Work Between Antigravity CLI and Claude Code
An unattended job sat waiting on an approval prompt for three days without a single error line. Here is how I stopped dividing work between two agent CLIs by capability, and started dividing it by where the approval boundary falls.
⚙ AI Tools2026-09-11
Starting with AGY_CLI_HIDE_LOGO: tuning Antigravity CLI for narrow terminals, screen readers, and recordings
Logo art, your email address, output that swallows the pane, a copy you never asked for. Here are the Antigravity CLI environment variables and settings that quiet the display, grouped by the three situations where they matter.
⚙ AI Tools2026-07-26
Routing /effort by Task Class in Antigravity CLI: Six Weeks of Measurements
I built a small router that picks an /effort level from the shape of the task, then aggregated six weeks of run logs. Here is where raising effort helped, where it actively hurt, and what mattered more than effort.