setup: silent codesign failure leaves Apple Silicon binaries unsigned, SIGKILL on first run
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## 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 && codesign -s - -f ` 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)
```bash
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:
```bash
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
```bash
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
```
Contributor guide
Research direction
Read the setup script around lines 247-264 and inspect test/setup-codesign.test.ts; first reproduce the signing check on macOS arm64. Done means every existing binary in the signing loop is signed after repeated gstack setup runs, failures are visible, and the setup test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, macos, shell, typescript
- Domain
- devops, security, testing-qa
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100