feat: RFC 0060 reliable binary installation

- install.sh: Add xattr/codesign fix after cp on macOS
- install.sh: Add portable timeout verification using perl
- INSTALL.md: Recommend cargo install --path as primary method
- INSTALL.md: Document macOS signature issue and fix
- blue doctor: Detect com.apple.provenance xattr with fix hint
- blue doctor: Verify code signature with codesign --verify
- blue doctor: 3-second liveness timeout for hanging binaries

Fixes dyld hang at _dyld_start when copied binaries have stale
signatures after Homebrew library updates.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Garcia 2026-02-05 21:01:19 -05:00
parent b0e533849c
commit c745d11870
4 changed files with 355 additions and 100 deletions

View file

@ -0,0 +1,135 @@
# RFC 0060: Reliable Binary Installation
| | |
|---|---|
| **Status** | Accepted |
| **Date** | 2026-02-06 |
| **Relates To** | RFC 0052 (CLI Hook Management), RFC 0049 (Synchronous Guard) |
---
## Summary
Fix the binary installation flow so it doesn't produce binaries that hang on macOS. Add `cargo install --path` support as the primary install method, and post-copy re-signing as a fallback safety net.
## Problem Statement
### The Bug
When the Blue binary is copied to `~/.cargo/bin/` (or `/usr/local/bin/`), macOS preserves stale extended attributes (`com.apple.provenance`) and the adhoc code signature from the original build. If Homebrew updates a dynamically linked library (e.g., `openssl@3`) between when the binary was built and when it's run, `dyld` hangs indefinitely at `_dyld_start` during signature verification. The process never reaches `main()`.
### Observed Behavior
```
$ blue init
[hangs forever — no output, 0 bytes written to stdout/stderr]
$ sample $PID
890 _dyld_start (in dyld) + 0 # 100% of samples stuck here
```
- `kill -9` cannot terminate the process (state: `UE` — uninterruptible + exiting)
- The same binary works fine when run from `target/release/blue` directly
- The same binary works fine when copied to `/tmp/` without extended attributes
### Root Cause
`cp` on macOS preserves extended attributes by default. The combination of:
1. `com.apple.provenance` xattr (marks binary as "downloaded/copied")
2. Stale adhoc linker signature from original `cargo build`
3. Updated dylib versions on disk (Homebrew `openssl@3`)
...causes `dyld` to enter a signature verification path that deadlocks.
### Evidence
| Test | Result |
|------|--------|
| `target/release/blue init` | Works (0.1s) |
| `cp` to `~/.cargo/bin/blue` then run | Hangs at `_dyld_start` |
| `cp` to `/tmp/blue-copy` then run | Works (no provenance xattr) |
| Symlink to `target/release/blue` | Works |
| `xattr -cr` + `codesign --force --sign -` | Works |
## Proposal
### 1. Support `cargo install` as primary method
Add workspace metadata so `cargo install --path apps/blue-cli` works correctly. This lets Cargo handle the build-and-install atomically, producing a freshly signed binary.
**INSTALL.md** becomes:
```bash
# Build and install (recommended)
cargo install --path apps/blue-cli
# Then configure for Claude Code
blue install
```
### 2. Post-copy re-signing in `install.sh`
For users who prefer `install.sh` or `cp`:
```bash
cp "$BINARY" "$INSTALL_DIR/blue"
# Fix macOS code signature after copy
if [[ "$OSTYPE" == "darwin"* ]]; then
xattr -cr "$INSTALL_DIR/blue"
codesign --force --sign - "$INSTALL_DIR/blue"
fi
```
### 3. Post-copy re-signing in `blue install` (Rust)
The `handle_install_command` currently doesn't copy the binary anywhere — it only sets up hooks, skills, and MCP config. But the `SessionStart` hook adds `target/release/` to PATH, which is fragile. Instead:
- Add an optional `--binary` flag to `blue install` that copies and re-signs the binary to `~/.cargo/bin/`
- Or detect if running from `target/release/` and warn the user
### 4. `blue doctor` validation
Add a check to `blue doctor` that detects:
- Stale code signatures on the installed binary
- Mismatched binary vs source versions
- Presence of `com.apple.provenance` xattr on the binary
## Implementation Plan
### Phase 1: Fix `install.sh` (immediate)
1. Add `xattr -cr` + `codesign --force --sign -` after every `cp` of the binary
2. Add a verification step that actually runs `blue --version` with a timeout
### Phase 2: Support `cargo install --path`
1. Verify `cargo install --path apps/blue-cli` works with the current workspace layout
2. Update `INSTALL.md` to recommend `cargo install` as the primary method
3. Update `install.sh` to use `cargo install` instead of `cp` when possible
### Phase 3: Harden `blue doctor`
1. Add macOS signature check: `codesign --verify` on the installed binary
2. Add xattr check: warn if `com.apple.provenance` is present
3. Add timeout-based liveness check: run `blue --version` with a 3s timeout
## Files Changed
| File | Change |
|------|--------|
| `install.sh` | Add post-copy re-signing for macOS |
| `apps/blue-cli/src/main.rs` | Add doctor checks for signature issues |
| `INSTALL.md` | Recommend `cargo install --path` as primary method |
## Test Plan
- [x] `install.sh` produces a working binary on macOS after Homebrew openssl update
- [x] `cargo install --path apps/blue-cli` produces a working binary
- [x] `blue doctor` detects provenance xattr on binary (warns with fix hint)
- [x] `blue doctor` passes on a freshly installed binary
- [x] Install flow works on Linux (no-op for codesign steps via `#[cfg(target_os = "macos")]`)
---
*"Right then. Let's get to it."*
-- Blue

View file

@ -1,144 +1,151 @@
# Installing Blue
## Quick Install
## Quick Install (Recommended)
```bash
./install.sh
```
# Build and install Blue (handles code signing correctly)
cargo install --path apps/blue-cli
This builds and installs both:
- **Blue CLI** to `/usr/local/bin/blue`
- **Blue MCP** configured for Claude Code
# Configure for Claude Code
blue install
```
Restart Claude Code after installation.
## Alternative: Build Then Install
If you prefer to build separately:
```bash
# Build Blue
cargo build --release
# Install for Claude Code (from build directory)
./target/release/blue install
```
Note: Running from `target/release/` works but the binary path may change after `cargo clean`. For a persistent installation, use `cargo install` above.
## What Gets Installed
### CLI
`blue install` configures everything for Claude Code:
The `blue` command becomes available system-wide:
| Component | Location | Purpose |
|-----------|----------|---------|
| **Hooks** | `.claude/hooks/` | Session lifecycle, write guards |
| **Settings** | `.claude/settings.json` | Project configuration |
| **Skills** | `~/.claude/skills/` | Alignment dialogues |
| **MCP Server** | `~/.claude.json` | Blue tools for Claude |
### Hooks
- `session-start.sh` — Injects Blue context at session start
- `guard-write.sh` — Validates file writes (RFC 0038)
### Skills
- `alignment-play` — Run multi-expert alignment dialogues
- `alignment-expert` — Marker syntax for expert agents
### MCP Tools
After restart, Claude has access to Blue tools:
- `blue_status`, `blue_next` — Project state
- `blue_rfc_*` — RFC management
- `blue_worktree_*` — Git worktree coordination
- `blue_pr_create` — Pull request creation
- `blue_dialogue_*` — Alignment dialogues
## System-Wide Install
To make `blue` available everywhere:
```bash
blue --version # Check installation
blue realm status # Realm commands
blue session start # Session management
blue daemon start # Background service
# Recommended: cargo install handles signing correctly
cargo install --path apps/blue-cli
# The binary is installed to ~/.cargo/bin/blue
# Ensure ~/.cargo/bin is in your PATH
```
### MCP Server
### Manual Copy (Not Recommended on macOS)
Claude Code configuration is created/updated at `~/.config/claude-code/mcp.json`:
```json
{
"mcpServers": {
"blue": {
"command": "blue",
"args": ["mcp"]
}
}
}
```
After restart, Claude has access to 8 realm tools:
- `realm_status`, `realm_check`, `contract_get`
- `session_start`, `session_stop`
- `realm_worktree_create`, `realm_pr_status`
- `notifications_list`
## Manual Install
### Build
If you must copy the binary manually on macOS, you need to fix the code signature:
```bash
# Build
cargo build --release
```
### Install CLI
```bash
# Standard location
# Copy and fix signature
sudo cp target/release/blue /usr/local/bin/
# Or custom location
cp target/release/blue ~/bin/
sudo xattr -cr /usr/local/bin/blue
sudo codesign --force --sign - /usr/local/bin/blue
```
### Configure MCP
Create `~/.config/claude-code/mcp.json`:
```json
{
"mcpServers": {
"blue": {
"command": "blue",
"args": ["mcp"]
}
}
}
```
If blue isn't in PATH, use the full path:
```json
{
"mcpServers": {
"blue": {
"command": "/path/to/blue",
"args": ["mcp"]
}
}
}
```
## Custom Install Location
```bash
INSTALL_DIR=~/bin ./install.sh
```
Without the signature fix, the binary may hang at startup (see RFC 0060).
## Uninstall
```bash
# Remove CLI
sudo rm /usr/local/bin/blue
blue uninstall
```
# Remove MCP config (or edit to remove blue entry)
rm ~/.config/claude-code/mcp.json
Or manually:
```bash
# Remove hooks
rm -rf .claude/hooks/
# Remove MCP config
# Edit ~/.claude.json and remove "blue" entry
# Remove skills
rm ~/.claude/skills/alignment-play
rm ~/.claude/skills/alignment-expert
# Remove binary (if using cargo install)
cargo uninstall blue
# Remove Blue data (optional)
rm -rf ~/.blue
rm -rf .blue/
```
## Requirements
- Rust toolchain (cargo)
- macOS, Linux, or Windows with WSL
- Claude Code (for MCP features)
- Claude Code
## Verify Installation
```bash
# CLI
blue --version
# Check installation health
blue doctor
```
# MCP (in Claude Code)
Human: What realm tools do you have?
Claude: I have realm_status, realm_check, contract_get...
Or in Claude Code:
```
Human: blue status
Claude: [calls blue_status] Project: blue, Branch: develop...
```
## Troubleshooting
**"command not found: blue"**
- Ensure `/usr/local/bin` is in your PATH
- Or use `INSTALL_DIR=~/bin ./install.sh` and add `~/bin` to PATH
- Use `cargo install --path apps/blue-cli` (adds to ~/.cargo/bin)
- Or run from the project directory: `./target/release/blue install`
- Ensure `~/.cargo/bin` is in your PATH
**Binary hangs on macOS (no output)**
- This is a code signature issue (RFC 0060)
- Fix: `xattr -cr $(which blue) && codesign --force --sign - $(which blue)`
- Or reinstall with: `cargo install --path apps/blue-cli --force`
**MCP tools not appearing in Claude**
- Restart Claude Code after installation
- Check `~/.config/claude-code/mcp.json` syntax
- Run `blue doctor` to check configuration
- Verify `blue mcp` runs without errors
**Permission denied**
- The installer uses sudo for `/usr/local/bin`
- Or install to a user directory: `INSTALL_DIR=~/bin ./install.sh`
**Hooks not firing**
- Check `.claude/hooks/` exists with executable scripts
- Run `blue install --force` to regenerate hooks

View file

@ -3270,8 +3270,85 @@ async fn handle_doctor_command() -> Result<()> {
// Check binary
println!("Binary:");
if let Ok(path) = which::which("blue") {
let binary_path = which::which("blue").ok();
if let Some(ref path) = binary_path {
println!(" ✓ blue found at {}", path.display());
// RFC 0060: macOS-specific signature and liveness checks
#[cfg(target_os = "macos")]
{
// Check for stale provenance xattr
let xattr_output = std::process::Command::new("xattr")
.arg("-l")
.arg(path)
.output();
if let Ok(output) = xattr_output {
let attrs = String::from_utf8_lossy(&output.stdout);
if attrs.contains("com.apple.provenance") {
println!(" ⚠ com.apple.provenance xattr present (may cause hangs)");
println!(" hint: xattr -cr {} && codesign --force --sign - {}",
path.display(), path.display());
issues += 1;
}
}
// Check code signature validity
let codesign_output = std::process::Command::new("codesign")
.args(["--verify", "--verbose"])
.arg(path)
.output();
if let Ok(output) = codesign_output {
if output.status.success() {
println!(" ✓ Code signature valid");
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("invalid signature") || stderr.contains("modified") {
println!(" ✗ Code signature invalid or stale");
println!(" hint: codesign --force --sign - {}", path.display());
issues += 1;
} else if stderr.contains("not signed") {
println!(" - Binary not signed (may be fine)");
}
}
}
// Liveness check with timeout
use std::time::Duration;
let liveness = std::process::Command::new(path)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.and_then(|mut child| {
// Wait up to 3 seconds
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => return Ok(status.success()),
Ok(None) => {
if start.elapsed() > Duration::from_secs(3) {
let _ = child.kill();
return Ok(false);
}
std::thread::sleep(Duration::from_millis(50));
}
Err(e) => return Err(e),
}
}
});
match liveness {
Ok(true) => println!(" ✓ Binary responds within timeout"),
Ok(false) => {
println!(" ✗ Binary hangs (dyld signature issue)");
println!(" hint: cargo install --path apps/blue-cli --force");
issues += 1;
}
Err(e) => println!(" ⚠ Could not run liveness check: {}", e),
}
}
} else {
println!(" ✗ blue not found in PATH");
issues += 1;

View file

@ -30,13 +30,49 @@ else
sudo cp "$BINARY" "$INSTALL_DIR/blue"
fi
# Verify installation
if command -v blue &> /dev/null; then
echo -e "${GREEN}Installed successfully${NC}"
echo ""
blue --version 2>/dev/null || blue help 2>/dev/null | head -1 || echo "blue installed to $INSTALL_DIR/blue"
# RFC 0060: Fix macOS code signature after copy
# cp preserves stale xattrs and adhoc signatures, causing dyld hangs
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "Fixing macOS code signature..."
if [ -w "$INSTALL_DIR/blue" ]; then
xattr -cr "$INSTALL_DIR/blue" 2>/dev/null || true
codesign --force --sign - "$INSTALL_DIR/blue" 2>/dev/null || true
else
sudo xattr -cr "$INSTALL_DIR/blue" 2>/dev/null || true
sudo codesign --force --sign - "$INSTALL_DIR/blue" 2>/dev/null || true
fi
fi
# Verify installation with timeout (RFC 0060)
# A hanging binary won't respond in 3 seconds
# Note: Using perl for portable timeout (works on macOS and Linux)
run_with_timeout() {
local timeout_sec=$1
shift
perl -e 'alarm shift; exec @ARGV' "$timeout_sec" "$@" 2>/dev/null
}
echo "Verifying installation..."
BLUE_PATH="$INSTALL_DIR/blue"
if [ -x "$BLUE_PATH" ]; then
if run_with_timeout 3 "$BLUE_PATH" --version >/dev/null 2>&1; then
echo -e "${GREEN}Installed successfully${NC}"
"$BLUE_PATH" --version 2>/dev/null || echo "blue installed to $BLUE_PATH"
else
echo -e "${RED}Binary installed but failed verification (possible signing issue)${NC}"
echo "Try: xattr -cr $BLUE_PATH && codesign --force --sign - $BLUE_PATH"
exit 1
fi
elif command -v blue &> /dev/null; then
if run_with_timeout 3 blue --version >/dev/null 2>&1; then
echo -e "${GREEN}Installed successfully${NC}"
blue --version 2>/dev/null || echo "blue available in PATH"
else
echo -e "${RED}Binary in PATH but failed verification${NC}"
exit 1
fi
else
echo -e "${GREEN}Installed to $INSTALL_DIR/blue${NC}"
echo -e "${GREEN}Installed to $BLUE_PATH${NC}"
echo "Add $INSTALL_DIR to PATH if not already present"
fi