Skip to content

Conversation

@bborn
Copy link
Owner

@bborn bborn commented Jan 11, 2026

Summary

Add Sprites (Fly.io managed sandboxes) as an execution environment for Claude tasks. Sprites provide isolated VMs where Claude can run with --dangerously-skip-permissions safely.

Architecture: Sprite as execution environment only

┌─────────────────────┐              ┌─────────────────────┐
│   Local Machine     │              │       Sprite        │
│                     │              │                     │
│  ┌───────────────┐  │              │  ┌───────────────┐  │
│  │   task TUI    │  │              │  │    Claude     │  │
│  │   + daemon    │  │   executes   │  │  (isolated)   │  │
│  │   + database  │──┼──────────────▶  │               │  │
│  └───────────────┘  │              │  └───────┬───────┘  │
│         ▲           │              │          │          │
│         │           │   tail -f    │          ▼          │
│         └───────────┼──────────────┼── /tmp/task-hooks   │
│                     │   (hooks)    │                     │
└─────────────────────┘              └─────────────────────┘

Key changes:

  • Task app runs locally - database, TUI, daemon all stay on your machine
  • When SPRITES_TOKEN is set, Claude executes on sprite instead of local tmux
  • Hooks stream back via file tailing (/tmp/task-hooks.jsonl)
  • No database syncing needed - sprite is just a sandbox

New files:

  • internal/sprites/sprites.go - Shared token/client logic
  • internal/executor/executor_sprite.go - SpriteRunner + hook streaming
  • cmd/task/sprite.go - CLI commands for sprite management

Usage:

# Set token (once)
export SPRITES_TOKEN=<your-token>
# or: task sprite token <your-token>

# Run task normally - Claude now executes on sprite
task

# Manual sprite management
task sprite status   # Show sprite status
task sprite up       # Start/restore sprite
task sprite down     # Checkpoint and suspend
task sprite attach   # Interactive shell on sprite
task sprite destroy  # Delete sprite

Test plan

  • Set SPRITES_TOKEN and verify Claude runs on sprite
  • Verify hooks update task status in real-time
  • Test task sprite subcommands
  • Verify local execution still works without token

🤖 Generated with Claude Code

bborn and others added 4 commits January 10, 2026 18:08
Explores using Sprites (sprites.dev) as isolated cloud execution
environments for Claude instances. This could simplify or replace
the current `taskd` cloud deployment approach.

Key findings:
- Sprites provide VM-level isolation with persistent filesystems
- Designed specifically for AI agent workloads
- ~$0.46 for a 4-hour Claude session
- Could eliminate need for dedicated cloud server
- Hook callbacks would work via HTTP

Proposes phased implementation from PoC to full cloud mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for running Claude tasks on Fly.io Sprites - isolated
cloud VMs that enable dangerous mode safely.

Key features:
- `task sprite init <project>` - Create sprite, clone repo, setup deps
- `task sprite status` - Show sprite status for projects
- `task sprite destroy <project>` - Delete a project's sprite
- `task sprite attach <project>` - SSH into sprite's tmux session
- `task sprite sync <project>` - Pull latest code, update deps
- `task sprite checkpoint <project>` - Save sprite state

Architecture:
- One sprite per project (persistent dev environment)
- Claude runs in dangerous mode inside isolated VM
- Hook events stream via tail -f over WebSocket
- User input sent via tmux send-keys

Database:
- Added sprite_name and sprite_status columns to projects table

Executor:
- Automatically uses sprite if project has one configured
- Hook streaming via tail -f /tmp/task-hooks.log
- Polls for completion, respects status from hooks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify the Sprites integration from per-project sprites to a single
shared sprite that runs the entire task daemon + TUI.

Changes:
- Auto-connect to sprite when SPRITES_TOKEN is set
- Add --local flag to force local execution
- Remove per-project sprite complexity (executor_sprite.go, client wrapper)
- Sprite runs `task -l --dangerous` with TTY attached
- User's local machine acts as thin client to cloud sprite

This approach is simpler and more cost-effective: one sprite handles
all projects, dangerous mode is safe inside the isolated VM.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify sprites architecture: sprites are now only used as the
execution environment for Claude, not for running the entire task app.

Key changes:
- Task app runs locally (or on remote server), database stays local
- When SPRITES_TOKEN is set, executor runs Claude on sprite instead of local tmux
- Hooks stream back from sprite via tail -f on /tmp/task-hooks.jsonl
- No database syncing needed - sprite is just a sandbox for Claude

New files:
- internal/sprites/sprites.go - Shared sprite client/token logic
- internal/executor/executor_sprite.go - Sprite execution + hook streaming

Flow:
1. User runs `task` locally (normal)
2. Executor checks SPRITES_TOKEN at startup
3. If set, creates SpriteRunner and starts hook listener
4. When task runs, Claude executes on sprite in tmux session
5. Hooks write to file on sprite, executor tails and updates local DB

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jan 27, 2026
Review the new Sprites exec API and document how it can improve
the existing sprites integration in PR #103 and PR #160.

Key findings:
- Exec sessions replace tmux entirely (persistent, reconnectable)
- Filesystem API replaces shell-based file operations
- Services API replaces nohup+polling for long-running processes
- Network Policy API enables security restrictions
- Port notifications enable dev server URL forwarding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bborn
Copy link
Owner Author

bborn commented Jan 27, 2026

Sprites Exec API — opportunities for this PR

I reviewed the new Sprites exec API (docs.sprites.dev/api/dev-latest/exec/) and the sprites-go SDK source. The SDK already supports features that would simplify this PR significantly. Here's a detailed breakdown.


1. Replace tmux with native exec sessions

The exec API provides persistent sessions that survive client disconnections — exactly what tmux does here, but natively.

Current approach (executor_sprite.go:198-230):

// Start Claude in tmux on sprite
cmd := r.sprite.CommandContext(ctx, "tmux", "new-session", "-d", "-s", sessionName,
    "-c", workDir, "sh", "-c", claudeCmd)
cmd.CombinedOutput()

// Poll for completion every 2 seconds
ticker := time.NewTicker(2 * time.Second)
for {
    checkCmd := r.sprite.CommandContext(ctx, "tmux", "has-session", "-t", sessionName)
    if err := checkCmd.Run(); err != nil {
        break // session ended
    }
}

With exec API directly:

cmd := r.sprite.CommandContext(ctx, "claude",
    "--dangerously-skip-permissions", "-p", prompt)
cmd.Dir = workDir
cmd.Env = []string{
    fmt.Sprintf("TASK_ID=%d", task.ID),
    fmt.Sprintf("WORKTREE_PORT=%d", task.Port),
}

// Sessions persist after disconnect — no tmux needed
// cmd.SessionID() gives us the ID for reconnection later
stdout, _ := cmd.StdoutPipe()
cmd.Start()

// Store session ID for crash recovery
sessionID := cmd.SessionID()
db.SetTaskSessionID(task.ID, sessionID)

// Block until Claude exits — no polling
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
    db.AppendTaskLog(task.ID, "claude", scanner.Text())
}
cmd.Wait() // ExitMessage gives exact exit code

Benefits:

  • Removes tmux dependency from sprite setup (no apt-get install tmux)
  • No 2-second polling loop — Wait() blocks until process exits
  • Session ID stored for reconnection after local crash
  • Exit code available directly from ExitMessage

2. Crash recovery via session reconnection

If the local task daemon restarts, it currently loses track of running sprite tasks. The exec API fixes this:

// On executor restart, check for existing sessions
sessions, _ := client.ListSessions(ctx, spriteName)
for _, session := range sessions {
    if strings.HasPrefix(session.Command, "claude") && session.IsActive {
        // Reattach to running Claude session
        cmd := sprite.AttachSession(ctx, session.ID)
        stdout, _ := cmd.StdoutPipe()
        // Resume streaming logs to database...
    }
}

The GET /v1/sprites/{name}/exec endpoint returns all active sessions with their command, PID, working directory, and activity status. Combined with session IDs stored in the task database, this enables full crash recovery.


3. Replace file-tailing hooks with direct streaming or FS Watch

Current approach (executor_sprite.go:100-145):

// Tail hooks file on sprite via exec
cmd := r.sprite.CommandContext(r.hookCtx, "tail", "-n", "0", "-f", "/tmp/task-hooks.jsonl")
stdout, _ := cmd.StdoutPipe()
cmd.Start()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
    // parse JSON hook events
}

Option A — FS Watch WebSocket (recommended):
The Sprites API has a WS /v1/fs/watch endpoint. The SDK exposes this via sprite.Filesystem():

// Watch for file changes without running a command
watcher := sprite.Filesystem().Watch(ctx, "/tmp/task-hooks.jsonl")

Option B — Parse Claude's stdout directly:
Since Claude's output now streams through the exec session, hook-like events can be parsed from stdout instead of a sidecar file. This eliminates the hook file entirely.

Option C — Read via Filesystem API:

// Periodic read without exec
data, _ := sprite.Filesystem().ReadFile("/tmp/task-hooks.jsonl")

4. Use Filesystem API for sprite setup

Current approach (sprite.go:265-345) shells out for file operations:

installHookCmd := fmt.Sprintf(`cat > /usr/local/bin/task-sprite-hook << 'HOOKEOF'\n%s\nHOOKEOF\nchmod +x ...`, hookScript)
cmd := sprite.CommandContext(ctx, "sh", "-c", installHookCmd)

With Filesystem API:

fs := sprite.Filesystem()
fs.WriteFile("/usr/local/bin/task-sprite-hook", []byte(hookScript), 0755)
fs.WriteFile("/root/.claude/settings.json", []byte(claudeSettings), 0644)
fs.MkdirAll("/workspace", 0755)

No shell escaping, no heredocs, no chmod as separate command. The WriteFile call accepts a file mode directly.


5. Network Policy for sprite security

The design doc mentions network restrictions but doesn't implement them. The Policy API makes this trivial:

client.SetPolicy(ctx, spriteName, &sprites.Policy{
    AllowedDomains: []string{
        "github.com",
        "api.anthropic.com",
        "rubygems.org",
        "registry.npmjs.org",
    },
})

This is one of the main security benefits of sprites — worth implementing.


6. Port notifications for dev servers

When Claude starts a dev server inside the sprite, the exec API sends a PortNotificationMessage with a proxy URL automatically:

cmd.TextMessageHandler = func(data []byte) {
    var notification sprites.PortNotificationMessage
    json.Unmarshal(data, &notification)
    if notification.Type == "port_opened" {
        db.AppendTaskLog(task.ID, "system",
            fmt.Sprintf("Dev server available at %s", notification.ProxyURL))
    }
}

This would let the TUI show clickable URLs when Claude starts servers — useful for web dev tasks.


SDK Note

The sprites-go SDK at the version pinned in this PR (v0.0.0-20260109202230) already has Cmd.SessionID(), Client.ListSessions(), Sprite.Filesystem(), Cmd.TextMessageHandler, Cmd.Env, and Cmd.Dir. These features are available without upgrading — they just aren't being used yet.

Full analysis with all code examples: #286

bborn added a commit that referenced this pull request Jan 27, 2026
Implement PR #103 feedback to use the Sprites exec API directly instead
of tmux-based execution. Key improvements:

1. **Native exec sessions instead of tmux**
   - Sessions persist after client disconnect
   - cmd.Wait() blocks until completion (no polling)
   - No tmux dependency needed on sprite

2. **Direct stdout streaming**
   - Stream Claude's output to task logs in real-time
   - Parse output for status changes
   - No file-tailing hooks needed

3. **Filesystem API for setup**
   - Use sprite.Filesystem().WriteFile() and MkdirAll()
   - No shell escaping or heredocs required

4. **Port notifications**
   - Handle PortNotificationMessage for dev servers
   - Automatically log proxy URLs when Claude starts servers

5. **Crash recovery foundation**
   - Track active tasks for graceful shutdown
   - Idle watcher for automatic checkpointing

New files:
- cmd/task/sprite.go - CLI commands for sprite management
- internal/sprites/sprites.go - Shared token/client logic
- internal/executor/executor_sprite.go - SpriteRunner with exec API
- docs/sprites-design.md - Architecture documentation
- docs/sprites-discussion.md - Design discussion

Usage:
  export SPRITES_TOKEN=<token>  # or: task sprite token <token>
  task                          # Claude now runs on sprite

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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.

2 participants