Skip to content

update-check crash is indistinguishable from "up to date" — preamble invocation fails open #1974

Closed

Description

@gaganawhad

Summary

The documented invocation of gstack-update-check (in skill preambles and the /gstack-upgrade standalone flow) is:

~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true

with the contract "no output = up to date". Because stderr is suppressed and the exit code is discarded, a script that crashes produces exactly the same observable result as a script that ran successfully and found no update. The check fails open.

Real-world incident

On a macOS install running Claude Code with command sandboxing enabled, ~/.gstack was not writable from inside the sandbox. gstack-update-check runs with set -euo pipefail and writes state to ~/.gstack early (snooze/marker/cache files), so it died on the first rm with Operation not permitted — before ever fetching the remote VERSION:

$ gstack-update-check --force; echo exit=$?
rm: /Users/<user>/.gstack/just-upgraded-from: Operation not permitted
exit=1

Wrapped in the documented 2>/dev/null || true chain, this rendered as silence → "up to date". The install sat at v1.12.2.0 while main was at v1.57.10.0 (45 releases, ~6 weeks behind), with every skill preamble cheerfully reporting no upgrade available. It was only caught because the user manually insisted an update existed and the agent compared VERSION against origin/main directly.

This failure mode is generic: any environment where the script exits non-zero (read-only $HOME, sandboxed agents, broken gstack-config, missing find/awk, etc.) silently disables update checks forever.

Suggested fix

Make failure distinguishable from "no update":

  1. In gstack-update-check, trap errors and emit a sentinel line on any abnormal exit, e.g. CHECK_FAILED <reason> on stdout (stdout isn't suppressed by the documented invocation), so callers/agents can react instead of inferring "up to date".
  2. Update the preamble/SKILL.md contract from "no output = up to date" to three states: UPGRADE_AVAILABLE, UP_TO_DATE (could be emitted explicitly), and CHECK_FAILED — and instruct agents to fall back to a direct git fetch && git show origin/main:VERSION comparison when the check fails.
  3. Optionally: degrade gracefully when ~/.gstack is unwritable (skip cache/snooze writes instead of dying) so the remote comparison still runs read-only.

Environment: macOS (Darwin 25.3), global-git install at ~/.claude/skills/gstack, Claude Code with bash sandboxing.

🤖 Filed via Claude Code after debugging the incident above.

Activity

  1. time-attack commented on Jul 15, 2026

    @time-attack
    Contributor

    @16francej — recommend closing this issue in favor of #1982, but it should close only after the implementation emits one bounded, redacted stage code and updates the resolver and generated contracts. Current behavior hides local update-check failures as ordinary silence. Verify local-failure and quiet-network controls without changing disabled, snoozed, or network-unavailable behavior.

  2. added a commit that references this issue on Aug 15, 2026
    008dd65
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions