fix: approval-required prints to stderr and exits 2 (was stdout + exit 0)
Problem
insta secrets --print --json > creds.json
echo $? # 0 — script thinks it succeeded
cat creds.json
# approval required for secrets.read — run: insta approvals approve 64749a5a-…
Three contract violations at once, shared by every handleApproval caller (34 sites across branch/db/compute/deploy/secrets/storage/project/services):
- The approval hint went to stdout (
handleApproval→info(),util.ts), so any redirect swallowed it. - Callers did
if (handleApproval(res)) return→ exit 0 on a pending gate — scripts and&&-chains read "gated, nothing happened" as success. - Under
--json, the hint printed before the json branch, so stdout carried non-JSON.
Meanwhile run.ts used die() (stderr + exit 1) and compute exec's applyExecResult had its own 202 branch (envelope + exit 1) — three different behaviors for the same platform response.
Fix
handleApproval now owns the whole 202 contract in one place:
- hint → stderr; stdout stays clean for redirects and parsers
process.exitCode = 2— distinct fromdie()'s generic 1, so scripts/agents can branch on "approvable, re-run after approval"--json→ stdout carries the platform's raw 202 envelope (approvalId/action/nextActions), so scripted callers lift the id without scraping stderr
Call sites with a --json flag pass it through (22 sites); applyExecResult's 202 branch collapses into if (handleApproval(res, json)) return (exec's approval exit moves 1 → 2 — now distinguishable from the remote command's own exit 1, which keeps 0–255 pass-through untouched); run.ts's bespoke die() path is unified.
Verification
- TDD: rewrote the
handleApprovalunit tests and the twoapplyExecResult202 tests first (red), then implemented (green). Full suite: 64 files / 712 tests pass. - Live against prod with
secrets.read=approveon a real project:--jsonvariant: redirected stdout = valid JSON envelope, hint on stderr, exit 2- original repro (
--print > file): 0-byte stdout, hint on stderr, exit 2 - cleanup: policy restored to allow, pending approval denied
Follow-up (separate PRs)
- skills docs: document the exit-2 + stderr + json-envelope contract in the approval-relay section (insta-skills)
- the platform's 202
messagefield itself says "have an admin run … then retry" — unchanged here
🤖 Generated with Claude Code
Summary by cubic
Approval-required responses now write the hint to stderr and set exit code 2. With --json, stdout carries the raw 202 envelope. This prevents scripts from treating a pending gate as success and keeps stdout parseable; --json is added across mutating commands with progress/diagnostics moved to stderr.
- Centralizes 202 handling in
handleApproval(res, json): stderr hint,process.exitCode = 2; with--json, stdout prints the envelope (approvalId,action,nextActions). Callers pass their--jsonflag through.compute execnow delegates tohandleApproval(approval exit 1 → 2);runuses it and moves its banner to stderr. - Adds
--jsonto: deploy, approvals approve/deny, project create/delete/link, branch create/delete/merge/switch, secrets set/unset, services remove, compute remove-domain, policy set, org create, env use. With--json, stdout emits one JSON document; progress and setup notes go to stderr. - Deploy
--json: returns{ image, machineId, url, branch, group, nextActions }; build progress tees to stderr viastderrBuildRunner.ensureFlyctland the project auto-link note write to stderr to avoid polluting JSON. - Fixes
compute remove-domain --jsonto print only the platform response viarenderRemoveDomain. Stabilizesenv use --jsonschema withenvUseResult(same keys for no-op and real switch).project create --jsonhard-errors when no name is resolvable.
Migration
- Treat exit code 2 as “approval required; retry after approval.” With
--json, read the 202 envelope from stdout; do not scrape stderr. - When using
--json, parse JSON from stdout and read progress/diagnostics from stderr. Update any&&chains or success checks that assumed exit 0 on approvals; forcompute exec, do not rely on exit 1 to mean approval.