@juspay/shooter
v1.1.0
Published
Bidirectional communication server for Claude Code and iOS — push notifications, remote terminal, session viewing
Keywords
Readme
Shooter
Mobile push notifications and remote terminal access for AI coding sessions.
What is Shooter?
Shooter turns your phone into a remote control for AI coding sessions running on your dev machine. It delivers push notifications to iOS and Android when Claude Code or OpenCode events occur -- tool usage, permission requests, session completions -- and lets you approve or deny permission prompts directly from a notification. You can also launch remote terminal sessions, stream output in real time, and browse structured AI conversation history, all from a mobile-optimized web interface accessible anywhere through a Cloudflare Tunnel.
Features
- Push notifications -- Real-time alerts for tool usage, permission requests, session starts/stops, errors, and task completions (iOS via APNs, Android via FCM)
- Bidirectional permissions -- Approve or deny Claude Code permission prompts from your phone; the hook blocks until you respond
- Remote terminal -- Launch shell, Claude Code, or OpenCode sessions from your phone with full xterm.js rendering
- Terminal persistence -- PTY processes run in holder processes that survive server restarts; metadata persisted in SQLite
- Structured Chat view -- AI conversations rendered as message bubbles with tool-use cards and thinking indicators, parsed live from JSONL session files
- Session browser -- Browse coding session history across all projects
- QR code pairing -- Scan a QR code from the
/configpage to connect mobile apps to the server - WebSocket streaming -- Three multiplexed channels: terminal I/O, session updates, and global events
- Quick keys -- Mobile-optimized touch bar for Ctrl+C, Tab, arrow keys, Esc, and other special characters
- Claude Code hooks -- Lifecycle hooks for 13 event types with context-aware notification categorization
- Docker support -- Multi-stage Dockerfile with arm64 and amd64 support
Quick Start
One-command install (recommended):
curl -fsSL https://raw.githubusercontent.com/juspay/shooter/release/scripts/install.sh | shThis clones to ~/.shooter/repo, auto-generates an API key, installs dependencies, builds, offers to install cloudflared for remote access, enables autostart on login, and starts the server.
Or clone and set up manually:
git clone https://github.com/juspay/shooter.git
cd shooter
pnpm install
pnpm setup # interactive wizard: generates .env, builds, runs health check
pnpm start # start the server on http://localhost:54007Open http://localhost:54007 in your browser. Visit /config to enter your API key for the web UI.
All Setup Methods
| Method | Command | Notes |
| ------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| One-command install | curl -fsSL https://raw.githubusercontent.com/juspay/shooter/release/scripts/install.sh \| sh | Recommended. Clones to ~/.shooter/repo, auto-generates API key, builds, installs cloudflared, starts server |
| Interactive wizard | pnpm setup | Walks through env config, builds, and verifies. Pass --auto for non-interactive mode. |
| CLI (npx) | npx @juspay/shooter setup | No clone needed -- runs the setup wizard directly from npm |
| Docker | docker compose up -d | See Docker |
| Manual | See Manual Setup | For advanced users |
Manual Setup
git clone https://github.com/juspay/shooter.git
cd shooter
pnpm install
cp .env.example .env
# Edit .env with your values (at minimum, set API_KEY)
pnpm build
pnpm startThe hook notifier reads API_KEY from the environment. Export it in your shell profile so hooks can authenticate with the server:
echo 'export API_KEY="your-api-key-here"' >> ~/.zshrc
source ~/.zshrcArchitecture
+----------------------------------------------------------+
| Dev Machine |
| |
| SvelteKit Server (adapter-node, port 54007) |
| +-- REST API (/api/terminals, /api/notify, ...) |
| +-- WebSocket Server (ws, noServer mode) |
| +-- PTY Manager (node-pty + holder processes) |
| +-- Terminal Store (SQLite persistence) |
| +-- Session Watcher (chokidar file watching) |
| +-- APNs Client (iOS push via @parse/node-apn) |
| +-- FCM Client (Android push via firebase-admin) |
+------------------------------+---------------------------+
|
Cloudflare Tunnel
shooter.yourdomain.com
|
+----------------------+----------------------+
| | |
+-------+--------+ +--------+-------+ +----------+------+
| Mobile Browser | | iOS App | | Android App |
| (web UI) | | (APNs push + | | (FCM push + |
| Terminal, Chat, | | permission | | WebView) |
| Session viewer | | responses) | | |
+-----------------+ +----------------+ +-----------------+Server entry point: server.ts creates an HTTP server wrapping the SvelteKit handler, attaches a WebSocket server in noServer mode, and handles upgrade requests with ticket-based authentication.
Terminal persistence: PTY processes run inside separate holder processes (pty-holder.cjs) that survive server restarts. Terminal metadata (ID, PID, command, cwd) is persisted in SQLite so the server can reattach on restart.
Three WebSocket channels:
| Channel | Path | Purpose |
| -------------- | ------------------ | ---------------------------------------------------- |
| Terminal I/O | /ws/terminal/:id | Raw PTY byte stream (xterm.js) |
| Session stream | /ws/session/:id | Structured AI conversation updates |
| Global events | /ws/events | Server broadcasts (new sessions, exits, permissions) |
Configuration
Copy .env.example to .env and fill in your values. The pnpm setup wizard handles this interactively.
| Variable | Required | Default | Description |
| ---------------------- | -------- | ------- | ---------------------------------------------------------------- |
| API_KEY | Yes | -- | Bearer token for authenticating all API and hook requests |
| PORT | No | 54007 | HTTP server port |
| DEVICE_PLATFORM | No | ios | Push notification target: ios or android |
| APNS_KEY | No | -- | APNs private key (.p8 file contents, newlines escaped as \n) |
| APNS_KEY_ID | No | -- | 10-character APNs key identifier from Apple Developer portal |
| APNS_TEAM_ID | No | -- | 10-character Apple Team ID |
| APNS_BUNDLE_ID | No | -- | iOS app bundle identifier (must match Xcode project) |
| APNS_PRODUCTION | No | false | Set true for TestFlight / App Store builds |
| DEVICE_TOKEN | No | -- | Target iOS device token (64-character hex) |
| FCM_PROJECT_ID | No | -- | Firebase project ID |
| FCM_CLIENT_EMAIL | No | -- | Firebase service account email |
| FCM_PRIVATE_KEY | No | -- | Firebase service account private key (PEM format) |
| ANDROID_DEVICE_TOKEN | No | -- | Target Android FCM device token |
iOS Setup
Prerequisites
- macOS with Xcode installed
- Apple Developer account with Push Notifications capability
- Physical iOS device (push notifications do not work in the simulator)
APNs Key Setup
- Go to Apple Developer > Keys and create a new key with Apple Push Notifications service (APNs) enabled
- Download the
.p8file - Note the Key ID (10 characters) shown after creation
- Find your Team ID in Membership Details
Add these to your .env:
APNS_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
APNS_KEY_ID=ABC123DEFG
APNS_TEAM_ID=XYZ789KLMN
APNS_BUNDLE_ID=com.yourcompany.shooter
DEVICE_TOKEN=<64-char-hex-from-device>Building the iOS App
cd ios/Shooter
open Shooter.xcodeproj- Select your signing team in Signing & Capabilities
- Ensure the Push Notifications capability is enabled
- Build and run on a physical device
- The device token is printed to the Xcode console on first launch
For TestFlight or App Store builds, set APNS_PRODUCTION=true in your server .env to route through the production APNs gateway.
Android Setup
Prerequisites
- Android Studio
- Gradle 8.12+ (for generating the wrapper)
- Firebase project with Cloud Messaging enabled
Firebase Setup
- Create a project in the Firebase Console
- Add an Android app with application ID
com.shooter.android - Download
google-services.jsonand place it inandroid/app/ - Go to Project Settings > Service Accounts and generate a new private key
- Copy
project_id,client_email, andprivate_keyfrom the downloaded JSON into your.env:
FCM_PROJECT_ID=your-firebase-project-id
FCM_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project.iam.gserviceaccount.com
FCM_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
ANDROID_DEVICE_TOKEN=<fcm-device-token>
DEVICE_PLATFORM=androidBuilding the Android App
cd android
chmod +x setup.sh
./setup.sh # generates Gradle wrapper
./gradlew assembleDebugThe app targets SDK 35 (min SDK 26) and uses a WebView that connects to your Shooter server URL.
Claude Code Hooks
Shooter integrates with Claude Code through lifecycle hooks defined in .claude/settings.json. A unified notifier script (.claude/hooks/notifier.cjs) handles all hook events.
Captured Events
| Hook | Description |
| -------------------- | --------------------------------------------------------------- |
| PreToolUse | Before a tool executes (file edit, bash command, etc.) |
| PostToolUse | After a tool completes successfully |
| PostToolUseFailure | After a tool fails |
| PermissionRequest | Claude Code asks for permission -- blocks until you respond |
| SessionStart | A new coding session begins |
| SessionEnd | A coding session ends |
| Stop | Claude Code stops execution |
| Notification | General notification from Claude Code |
| SubagentStart | A subagent is spawned |
| SubagentStop | A subagent completes |
| UserPromptSubmit | User submits a prompt |
| TeammateIdle | A teammate agent becomes idle |
| TaskCompleted | A task finishes |
| PreCompact | Before context compaction |
Permission Flow
- Claude Code triggers
PermissionRequesthook - Notifier sends a push notification with the tool name and details to your phone
- You tap Allow or Deny on the interactive notification (iOS) or in the app
- Notifier polls
GET /api/response?requestId=...until your decision arrives - The hook returns the decision to Claude Code, which proceeds or aborts
The PermissionRequest hook has a 180-second timeout in .claude/settings.json. The notifier's internal poll timeout is 120 seconds, providing a 60-second safety buffer.
Hook Environment Variables
| Variable | Default | Description |
| ---------------------------- | ------- | ----------------------------------------------------------- |
| SHOOTER_USE_LOCAL | -- | Set true to connect to local server instead of remote URL |
| SHOOTER_LOCAL_PORT | 54007 | Local server port when using SHOOTER_USE_LOCAL |
| SHOOTER_API_URL | -- | Remote server URL (when not using local) |
| SHOOTER_PERMISSION_TIMEOUT | 120 | Seconds to wait for a permission response |
| API_KEY | -- | Bearer token (must match the server's API_KEY) |
Docker
Quick Start
cp .env.example .env
# Edit .env with your values
docker compose up -dManual Build and Run
docker build -t shooter .
docker run -d \
--name shooter \
--env-file .env \
-p 54007:54007 \
-v shooter-data:/root/.shooter \
--restart unless-stopped \
shooterThe multi-stage Dockerfile uses node:20-slim and includes build tools for node-pty and better-sqlite3 native addons. SQLite data is persisted in the shooter-data volume. The .env file is injected at runtime and never baked into the image.
A separate Dockerfile.test is provided for verifying the fresh-user install experience in an isolated container.
docker-compose.yml
services:
shooter:
build: .
ports:
- '54007:54007'
env_file:
- .env
volumes:
- shooter-data:/root/.shooter
restart: unless-stopped
volumes:
shooter-data:API Reference
All endpoints require the Authorization: Bearer <API_KEY> header.
| Method | Path | Description |
| -------- | --------------------------- | ---------------------------------------------------- |
| GET | /api/health | Health check with server status |
| GET | /api/terminals | List all active and recently exited terminals |
| POST | /api/terminals | Create a new terminal session |
| GET | /api/terminals/:id | Get details for a specific terminal |
| DELETE | /api/terminals/:id | Kill and remove a terminal session |
| POST | /api/terminals/:id/resize | Resize a terminal (cols, rows) |
| POST | /api/ws-ticket | Generate a short-lived WebSocket auth ticket |
| GET | /api/ws-status | Get connected WebSocket client count |
| POST | /api/notify | Send a push notification via APNs or FCM |
| GET | /api/notify | Check notification status and history |
| POST | /api/response | Submit a permission allow/deny decision |
| GET | /api/response | Poll for a pending permission decision |
| GET | /api/sessions | List sessions across all projects |
| POST | /api/webhook | Receive external webhook events |
| GET | /api/qr-config | Generate QR code for mobile app pairing |
| POST | /api/device-token | Register a device token (iOS or Android) |
| GET | /api/debug | Debug information (APNs config, device token status) |
WebSocket Authentication
WebSocket connections use ticket-based auth. First call POST /api/ws-ticket with your Bearer token to receive a single-use ticket (valid 30 seconds), then connect with ?ticket=TICKET in the query string.
Example: Create Terminal
curl -X POST http://localhost:54007/api/terminals \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "claude", "cwd": "/Users/me/project", "cols": 80, "rows": 24}'Response:
{
"id": "term_a1b2c3",
"pid": 45231,
"command": "claude",
"cwd": "/Users/me/project",
"ws": "/ws/terminal/term_a1b2c3",
"sessionWs": "/ws/session/term_a1b2c3",
"createdAt": "2026-03-17T10:00:00Z"
}Development
pnpm dev # Vite dev server with hot reload (no WebSocket server)
pnpm build # Production build (outputs to build/)
pnpm start # Production server with WebSocket support (tsx server.ts)
pnpm preview # Preview production build via Vite
pnpm check # TypeScript type checking
pnpm run gen:types # Generate types from YAML specs (specs/types/)
pnpm lint # ESLint
pnpm lint:fix # ESLint with auto-fix
pnpm format # Prettier formatting
pnpm format:check # Check formatting without writingNote: pnpm dev runs the Vite dev server, which does not include the WebSocket server or PTY manager. For full functionality (terminal sessions, live streaming), use pnpm build && pnpm start.
CLI Commands
The shooter command (via bin/shooter.cjs or the global shooter symlink) supports:
| Command | Description |
| -------------------- | ------------------------------------------------------------------ |
| shooter start | Start the server (default if no command given) |
| shooter stop | Stop the running server gracefully (SIGTERM, then SIGKILL after 5s)|
| shooter status | Show PID, URL, autostart state, log path |
| shooter autostart on | Enable autostart on login (LaunchAgent on macOS, systemd on Linux) |
| shooter autostart off | Disable autostart and remove the service definition |
| shooter logs | Tail server logs (log file on macOS, journalctl on Linux) |
| shooter setup | Run the interactive setup wizard; pass --auto for non-interactive|
| shooter version | Print version number |
| shooter help | Show all available commands |
Process state is tracked via a PID file at ~/.shooter/shooter.pid. Logs are written to ~/.shooter/logs/shooter.log when running via autostart.
Type System
Types are auto-generated from YAML specifications in specs/types/ using type-crafter. Never edit files in src/generated/types/ directly -- edit the YAML specs and run pnpm run gen:types.
Project Structure
shooter/
server.ts # HTTP + WebSocket server entry point (build guard check)
package.json # Dependencies and scripts (pnpm only)
Dockerfile # Multi-stage Docker build
Dockerfile.test # Test image for fresh-user install verification
docker-compose.yml # Docker Compose config
.env.example # Environment variable template
svelte.config.js # SvelteKit config (adapter-node)
vite.config.ts # Vite config (node-pty external)
bin/
shooter.cjs # CLI entry point (start|stop|status|autostart|logs|setup|help)
scripts/
setup.cjs # Interactive setup wizard (--auto for non-interactive)
install.sh # One-command installer (full auto setup + cloudflared)
.claude/
hooks/notifier.cjs # Unified hook notifier (Node.js)
settings.json # Hook configuration (13 event types)
src/
generated/types/ # Auto-generated TypeScript types (DO NOT EDIT)
lib/
modules/
server/
apn/ # APNs push notification service
auth.ts # Shared authentication helper
cli/ # CLI command utilities
terminal/
pty-manager.ts # PTY lifecycle, scrollback, cleanup
pty-holder.cjs # Standalone holder process for persistence
terminal-store.ts # SQLite persistence for terminal metadata
session-watcher.ts # JSONL file watcher (chokidar)
opencode-watcher.ts # OpenCode session watcher
ws/
server.ts # WebSocket upgrade routing
terminal-handler.ts # Terminal I/O channel
session-handler.ts # Session stream channel
events-handler.ts # Global event bus channel
ticket-store.ts # One-time auth ticket store
keepalive.ts # Ping/pong heartbeat
sessions/
jsonl-reader.ts # Parse JSONL session files
opencode-reader.ts # Parse OpenCode sessions
client/
common/ # Reusable UI components
terminal/
ChatView.svelte # Structured AI conversation view
LaunchSheet.svelte # Terminal launch dialog
QuickKeys.svelte # Mobile quick key bar
ConnectionStatus.svelte # Connection state indicator
xterm-wrapper.ts # Async xterm.js initialization
routes/
api/ # REST API endpoints (17 endpoints)
terminals/ # Terminal list and detail pages
project/ # Project dashboard
session/[id]/ # Session viewer
config/ # Settings page with QR pairing
specs/types/ # Type-crafter YAML specifications
ios/Shooter/ # Swift iOS app (Xcode project)
android/ # Kotlin Android app (Gradle project)
docs/ # Documentation
plans/ # Architecture plans and roadmapTroubleshooting
Server does not start
- Verify Node.js 20+ is installed:
node --version - Ensure pnpm is used (npm and yarn are blocked):
pnpm --version - Check that
pnpm buildcompleted without errors before runningpnpm start--server.tshas a build guard that exits with a clear error ifbuild/handler.jsis missing - Confirm
.envexists andAPI_KEYis set (the server also checks~/.shooter/.envas a fallback) - On Linux, ensure build tools are installed:
python3,make,g++(needed for native modules)
WebSocket connections fail
pnpm devdoes not run the WebSocket server. Usepnpm build && pnpm startfor full functionality.- Ensure you are obtaining a ticket via
POST /api/ws-ticketbefore connecting - Tickets expire after 30 seconds and are single-use
Push notifications not arriving
- iOS: Verify
APNS_KEY,APNS_KEY_ID,APNS_TEAM_ID,APNS_BUNDLE_ID, andDEVICE_TOKENare all set in.env - iOS (TestFlight/App Store): Set
APNS_PRODUCTION=true-- sandbox tokens do not work with the production gateway and vice versa - Android: Ensure
google-services.jsonis inandroid/app/and FCM credentials are in.env - Check
GET /api/debugfor APNs configuration status and device token validity - Check server logs for APNs or FCM error responses
Hooks not sending notifications
API_KEYmust be exported in your shell environment, not just in.env:export API_KEY="..."- Verify the hooks are configured in
.claude/settings.json - Test connectivity:
curl -H "Authorization: Bearer $API_KEY" http://localhost:54007/api/health
Terminal sessions lost after restart
- Terminal metadata is persisted in SQLite and PTY holder processes survive restarts, so running terminals are reattached automatically
- In-memory state (WebSocket connections, auth tickets, pending permission requests) is lost on restart
node-pty build errors
- Ensure Python 3, make, and a C++ compiler are installed
- On macOS, install Xcode Command Line Tools:
xcode-select --install - Try rebuilding:
pnpm rebuild node-pty
Port already in use
- Default port is 54007. Set
PORT=<number>in.envto use a different port. - Check what is using the port:
lsof -i :54007
Security
- Command allowlist -- Only
zsh,bash,sh,fish,claude, andopencodecan be launched as terminal commands - Ticket-based WebSocket auth -- Short-lived, single-use tickets (30-second expiry) keep API keys out of WebSocket URLs
- Bearer token on all REST endpoints -- Every request requires
Authorization: Bearer <API_KEY> - Working directory validation -- The
cwdparameter is validated against the user's home directory; symlink traversal is blocked - No credentials in code -- All secrets loaded from
.envat runtime;.envis gitignored - APNs JWT rotation -- Push notification tokens are generated with short expiry and rotated automatically
License
MIT
