Skip to content

setup: silent codesign failure leaves Apple Silicon binaries unsigned, SIGKILL on first run #1254

@lucascaro

Description

@lucascaro

Symptom

After gstack setup (fresh install or upgrade), Bun-compiled binaries in ~/.claude/skills/gstack/{browse,design,make-pdf,bin}/ are killed by macOS Gatekeeper with SIGKILL (exit status 137) on first invocation. The setup itself reports success, so the failure surfaces later when a skill tries to call $B, $D, etc.

Repro on my machine

macOS 14.x, Apple Silicon (arm64). gstack 1.15.0.0 (HEAD 6209163 in ~/.claude/skills/gstack/browse/dist/.version).

$ for b in browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover; do
    p="$HOME/.claude/skills/gstack/$b"
    codesign -dv "$p" 2>&1 | head -1
  done
/Users/.../browse/dist/browse: code object is not signed at all
/Users/.../browse/dist/find-browse: code object is not signed at all
/Users/.../design/dist/design: code object is not signed at all
/Users/.../make-pdf/dist/pdf: code object is not signed at all
/Users/.../bin/gstack-global-discover: code object is not signed at all

5/5 binaries unsigned despite the codesign loop in setup (added in #1056 / v0.18.4.0).

Manually running codesign --remove-signature <bin> && codesign -s - -f <bin> produces a working ad-hoc signature and the binary runs fine, so the codesign machinery itself works on this machine — it's the setup-time invocation that didn't take.

Root cause (3 compounding issues in setup lines 247-264)

codesign --remove-signature "$_bin_path" 2>/dev/null || true
if ! codesign -s - -f "$_bin_path" 2>/dev/null; then
  log "warning: codesign failed for $_bin (binary may not run on Apple Silicon)"
fi
  1. 2>/dev/null discards the actual codesign error. Whatever caused the failure on a given machine (transient lock, fs ACL, Xcode CLT state, etc.) is invisible — both to the user and to anyone trying to debug from a bug report.
  2. log "warning: ..." routes to the log channel, not the user's terminal. The setup completes with no visible indication that a critical step failed.
  3. The block lives inside the NEEDS_BUILD=1 branch. If a subsequent gstack setup invocation skips the build (binary exists from a previous half-broken install), the codesign block is also skipped — re-running setup doesn't self-heal a broken install.

The combined effect is that any transient codesign failure during the one setup run that built the binary leaves the install permanently broken from the user's perspective, with no user-visible signal until a skill actually calls into the binary and gets exit 137.

Suggested fix

Three small, defensive changes in setup:

  1. Move the codesign loop out of the NEEDS_BUILD=1 branch so it runs on every gstack setup. Idempotent and cheap (<1s for all 5 binaries).
  2. Capture codesign's stderr instead of discarding it; surface it on failure.
  3. After signing, verify with codesign -dv and fail the setup loudly if any binary remains unsigned. Don't claim "ready" for a half-broken install.

Sketch:

if [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "arm64" ]; then
  _sign_failed=()
  for _bin in browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover; do
    _bin_path="$SOURCE_GSTACK_DIR/$_bin"
    [ -f "$_bin_path" ] && [ -x "$_bin_path" ] || continue
    codesign --remove-signature "$_bin_path" >/dev/null 2>&1 || true
    if ! _err=$(codesign -s - -f "$_bin_path" 2>&1); then
      echo "codesign failed for $_bin: $_err" >&2
      _sign_failed+=("$_bin")
      continue
    fi
    if ! codesign -dv "$_bin_path" >/dev/null 2>&1; then
      echo "codesign reported success but binary remains unsigned: $_bin" >&2
      _sign_failed+=("$_bin")
    fi
  done
  if [ ${#_sign_failed[@]} -gt 0 ]; then
    echo "ERROR: gstack binaries failed to codesign — these will SIGKILL (exit 137) on first run:" >&2
    printf '  - %s\n' "${_sign_failed[@]}" >&2
    exit 1
  fi
fi

A test in test/setup-codesign.test.ts shape — assert that after gstack setup, every binary in the loop returns 0 from codesign -dv.

Workaround for affected users

cd ~/.claude/skills/gstack
for b in browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover; do
  [ -f "$b" ] && [ -x "$b" ] || continue
  codesign --remove-signature "$b" 2>/dev/null || true
  codesign -s - -f "$b"
  codesign -dv "$b" 2>&1 | head -1
done

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