Skip to content

fix(aside): the readiness probe can never detect Aside under zsh - #2824

Closed
AntonioVitalic wants to merge 1 commit into
garrytan:mainfrom
AntonioVitalic:fix/aside-probe-zsh-word-splitting
Closed

AntonioVitalic wants to merge 1 commit into
garrytan:mainfrom
AntonioVitalic:fix/aside-probe-zsh-word-splitting

Conversation

@AntonioVitalic

@AntonioVitalic AntonioVitalic commented Sep 7, 2026

Copy link
Copy Markdown

Fixes #2842.

Why (in your own words)

On any machine whose shell is zsh — the macOS default, and what agent harnesses run their shell tool as — the BROWSER SETUP probe answers ASIDE_NOT_RUNNING while Aside is running fine. The status line is the small part. The damage is that every browsing skill then silently drops to the bundled headless browser, which has none of the user's cookies or logged-in sessions, so a page behind a login comes back signed-out and the skill blames the site instead of the probe. #2810 made Aside the browser gstack drives first; under zsh that contract was unreachable, and Aside only ships for macOS, where zsh is the shell.

The probe holds its deadline in a variable and expands it unquoted, so it only becomes two words in a shell that word-splits unquoted expansions. zsh does not — it looks for one command named gtimeout 30. The variable is the vehicle for all three deadline arms, so under zsh every arm fails, including the perl arm that is the only one present on a stock Mac.

This change makes the deadline a function, which takes the command as "$@", already split, and behaves identically in sh, bash and zsh.

Live evidence

Environment: macOS 15 (Darwin 25.6.0), zsh 5.9, bash 3.2, aside 1.26.906.1630 — installed, signed in, answering aside repl in ~5 ms throughout. Each row narrows PATH so a different deadline arm is selected; the probe is extracted verbatim from the rendered browse/SKILL.md.

The mechanism, isolated:

$ _T="gtimeout 30"; $_T echo hi
zsh: command not found: gtimeout 30
$ ${=_T} echo hi          # zsh word-split forced, to confirm the cause
hi

Before — main at v1.87.0.0. Aside was up the whole time:

deadline arm            zsh                  bash    sh
gtimeout (coreutils)    ASIDE_NOT_RUNNING    READY   READY
timeout only            ASIDE_NOT_RUNNING    READY   READY
perl only (stock Mac)   ASIDE_NOT_RUNNING    READY   READY

After — this branch:

deadline arm            zsh      bash     sh
gtimeout (coreutils)    READY    READY    READY
timeout only            READY    READY    READY
perl only (stock Mac)   READY    READY    READY
none of the three       READY    READY    READY

$ GSTACK_SKIP_ASIDE=1 zsh probe.sh
NEEDS_ASIDE

READY is the full line READY: aside 1.26.906.1630 in every cell; trimmed here for width.

Why not eval. eval splits in all three shells and is the obvious one-liner — this PR carried it until I measured the perl arm. eval re-parses the string, so the parens and the ; in perl -e alarm(shift);exec(@ARGV) 30 stop being data and become syntax:

$ # with eval, perl arm selected
bash: syntax error near unexpected token `('
zsh:  unknown file attribute: h

deadline arm            eval, zsh            eval, bash
gtimeout                READY                READY
timeout only            READY                READY
perl only (stock Mac)   ASIDE_NOT_RUNNING    ASIDE_NOT_RUNNING

A stock Mac has no Homebrew coreutils and no GNU timeout, but does have /usr/bin/perl — so eval trades the zsh bug for a regression on the default macOS install, and takes bash down with it. Recorded here so the next person doesn't reach for it. The same reasoning rules out ${=_T} (zsh-only): anything that keeps the deadline a string has to re-split or re-parse it.

Tests:

$ bun test test/aside-driver.test.ts test/host-config.test.ts
 105 pass
 0 fail
 539 expect() calls

Scope

  • Changed: scripts/resolvers/aside.ts — the deadline becomes _gs_bounded(), same gtimeout → timeout → perl alarm order, plus a 4th arm that runs the call unbounded when none of the three exists (what the empty _T already did). test/aside-driver.test.ts — the assertion pinned the broken invocation verbatim, so it now pins the function, asserts the 4th arm, and asserts neither $_T aside repl nor an eval … aside repl form can come back. The 19 generated SKILL.md files and the 3 test/fixtures/golden/ fixtures are regenerated, not hand-edited (bun run gen:skill-docs; goldens rendered the way test/host-config.test.ts renders them).
  • Verified live by: running the probe extracted from the rendered browse/SKILL.md under zsh, bash and sh, once per deadline arm (PATH narrowed to select each), before and after, with Aside live — the tables above. Plus the opt-out path and the two test files.
  • Did NOT test: Linux or Windows hosts (Aside is macOS-only; there the probe short-circuits on command -v aside and the fallback browser takes over — that path is unchanged). No claude -p E2E run. I did not exercise a machine where aside exists but the app is genuinely down; the ASIDE_NOT_RUNNING branch is unchanged by this PR.

Liveness proof (required for external contributors)

Authenticated login: gh api user --jq .loginAntonioVitalic.

Checklist

Related

#2848 and #2854 report the same defect and land on the same shape of fix, which suggests a function is the answer rather than a taste call. Two notes for whoever triages:

  • fix: Aside readiness probe reports a false ASIDE_NOT_RUNNING on zsh #2854 says the perl arm is also broken under bash today. It isn't: an unquoted expansion is split on IFS, but its metacharacters are never re-parsed, so the ; stays inside a word and the arm runs. It breaks only once the string goes through eval — the table above.
  • The 4th arm (else "$@") is load-bearing: with none of the three present, today's empty _T still runs the call, and a function without that arm would drop it.

Introduced in #2810.

@trunk-io

trunk-io Bot commented Sep 7, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

welchsteven added a commit to welchsteven/gstack that referenced this pull request Sep 11, 2026
…ct Aside

The BROWSER SETUP probe stored its deadline wrapper as a string and ran it as
a bare word:

    _T="gtimeout 30"   # or "timeout 30", or "perl -e alarm(shift);exec(@argv) 30"
    $_T aside repl ...

zsh does not word-split unquoted parameter expansions, so it looks for one
command literally named "gtimeout 30", fails, and prints ASIDE_NOT_RUNNING
against a healthy Aside. Every browse-family skill then falls back to $B, and
zsh users (the macOS default shell) lose their real signed-in sessions.

The deadline is now a _gs_bounded() function passing "$@", which needs no word
splitting and behaves the same in zsh, bash and sh. The two string-preserving
fixes both regress someone: ${=_T} is zsh-only and breaks POSIX sh, and eval
breaks the stock-macOS path. The perl fallback string only works when it is
word-split and never re-parsed, so eval turns it into "syntax error near
unexpected token '('" in every shell, including bash users on a stock Mac who
detect Aside correctly today. Inside the function the perl program is quoted,
and the deadline is verified to fire (alarm 2 vs sleep 10: exit 142 after 2s).

The failure branch now captures the probe output and prints PROBE_OUTPUT,
so "No browser window is open for account ..." (Aside running, profile signed
out or windowless) is distinguishable from the app being down. Rule 2 already
asked agents to quote the probe output verbatim; the old grep -q discarded it.

test/aside-driver.test.ts asserted the buggy strings verbatim, which is how the
bug survived. It now asserts the function form and guards against the string
form returning. Regenerated with gen:skill-docs; the three ship goldens get the
same block by surgical replacement.

Refs garrytan#2842, garrytan#2824.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AntonioVitalic
AntonioVitalic force-pushed the fix/aside-probe-zsh-word-splitting branch 3 times, most recently from b61c94b to feaa30c Compare September 14, 2026 22:56
…Aside

The probe built its deadline into `_T` and expanded it unquoted, so
`$_T aside repl …` only worked in a shell that word-splits. zsh does not: it
looked for a command literally named "gtimeout 30", the probe answered
ASIDE_NOT_RUNNING with Aside installed and ready, and every browsing skill
fell back to the bundled Chromium in silence. zsh is the macOS default and
Aside is macOS-only, so on a stock Mac the probe could never report READY.

The deadline becomes a function. It receives the command as "$@", already
split, so sh, bash and zsh all behave the same, and the gtimeout → timeout →
perl alarm chain is unchanged. A 4th arm runs the call unbounded when none of
the three is present, which is what the empty `_T` did before.

Not `eval`: it re-parses the string, so the parens and `;` of the perl arm
become syntax and that arm dies in bash *and* zsh. perl is the arm that runs
on a stock Mac — no coreutils, no GNU timeout — so routing through eval trades
the zsh bug for a regression on the default macOS install.

The test pinned the broken invocation verbatim, so it now pins the function
and asserts neither `$_T aside repl` nor an eval form comes back.

The rationale for that shape lives in the resolver, not in the emitted bash. The
block ships inside 19 browsing skeletons, and four of them have less headroom
than one comment line: plan-ceo-review 48 bytes, plan-devex-review 55,
plan-eng-review 60, and qa 43 before its 1.08 union ratio. Emitting the
explanation cost 154 bytes per skeleton and put all four over their guards;
carrying it as a TS comment costs 32 and clears every one, so no budget moves
here.
@AntonioVitalic

Copy link
Copy Markdown
Author

Continued in #2870 — same branch, same fix, with the CI failure resolved.

This PR closed itself on the last push: an amended commit force-pushed from a shallow clone came out as a root commit (the amend drops the parent at the shallow boundary), the branch lost its common ancestor with main, and GitHub auto-closes on that. Reopening is refused from both sides — at the closing head, no history in common with garrytan:main; at the repaired head, the branch was force-pushed or recreated.

The history is repaired (the head is now a normal child of 4a3c6a8a, same tree, same author), so the work continues in #2870 rather than here. Nothing about the fix itself changed in the move.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aside probe always reports ASIDE_NOT_RUNNING under zsh: unquoted $_T timeout wrapper is never word-split

1 participant