https://github.com/juspay/shooter
Turn your phone into a remote control for AI coding sessions running on your dev machine.
https://github.com/juspay/shooter
ai claude-code development open-code productivity remote-control
Last synced: 28 days ago
JSON representation
Turn your phone into a remote control for AI coding sessions running on your dev machine.
- Host: GitHub
- URL: https://github.com/juspay/shooter
- Owner: juspay
- Created: 2025-08-03T17:41:46.000Z (10 months ago)
- Default Branch: release
- Last Pushed: 2026-05-09T12:05:29.000Z (about 1 month ago)
- Last Synced: 2026-05-09T14:15:45.228Z (about 1 month ago)
- Topics: ai, claude-code, development, open-code, productivity, remote-control
- Language: TypeScript
- Homepage:
- Size: 3.28 MB
- Stars: 3
- Watchers: 0
- Forks: 3
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Shooter
**Mobile push notifications and remote terminal access for AI coding sessions.**
[](https://kit.svelte.dev/)
[](https://www.typescriptlang.org/)
[](https://nodejs.org/)
[](#websocket-channels)
[](#docker)
[](LICENSE)
---
## 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 `/config` page 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):
```bash
curl -fsSL https://raw.githubusercontent.com/juspay/shooter/release/scripts/install.sh | sh
```
This 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:**
```bash
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:54007
```
Open [http://localhost:54007](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](#docker) |
| Manual | See [Manual Setup](#manual-setup) | For advanced users |
### Manual Setup
```bash
git clone https://github.com/juspay/shooter.git
cd shooter
pnpm install
pnpm setup # generates ~/.shooter/.env with API key, builds the project
pnpm start
```
Or without the wizard:
```bash
git clone https://github.com/juspay/shooter.git
cd shooter
pnpm install
mkdir -p ~/.shooter && echo "API_KEY=$(openssl rand -hex 32)" > ~/.shooter/.env
pnpm build
pnpm start
```
> **Note:** Configuration lives in `~/.shooter/.env` (not the repo root). The hook notifier reads `API_KEY` from this file automatically.
---
## Architecture
```
+----------------------------------------------------------+
| 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
Configuration is stored in `~/.shooter/.env`. The `pnpm setup` wizard generates this file interactively. Only `API_KEY` is required to start -- push notification config can be added later with `shooter setup --push`.
| 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
1. Go to [Apple Developer > Keys](https://developer.apple.com/account/resources/authkeys/list) and create a new key with **Apple Push Notifications service (APNs)** enabled
2. Download the `.p8` file
3. Note the **Key ID** (10 characters) shown after creation
4. Find your **Team ID** in [Membership Details](https://developer.apple.com/account/#/membership)
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
```bash
cd ios/Shooter
open Shooter.xcodeproj
```
1. Select your signing team in **Signing & Capabilities**
2. Ensure the **Push Notifications** capability is enabled
3. Build and run on a physical device
4. 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
1. Create a project in the [Firebase Console](https://console.firebase.google.com/)
2. Add an Android app with application ID `com.shooter.android`
3. Download `google-services.json` and place it in `android/app/`
4. Go to **Project Settings > Service Accounts** and generate a new private key
5. Copy `project_id`, `client_email`, and `private_key` from 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=
DEVICE_PLATFORM=android
```
### Building the Android App
```bash
cd android
chmod +x setup.sh
./setup.sh # generates Gradle wrapper
./gradlew assembleDebug
```
The 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
1. Claude Code triggers `PermissionRequest` hook
2. Notifier sends a push notification with the tool name and details to your phone
3. You tap **Allow** or **Deny** on the interactive notification (iOS) or in the app
4. Notifier polls `GET /api/response?requestId=...` until your decision arrives
5. 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
```bash
# Minimal — just set API_KEY:
echo "API_KEY=$(openssl rand -hex 32)" > .env
docker compose up -d
```
Or with a Cloudflare Tunnel for remote access:
```bash
docker compose --profile tunnel up -d
```
### Manual Build and Run
```bash
docker build -t shooter .
docker run -d \
--name shooter \
-e API_KEY=your-secret-key-here \
-p 54007:54007 \
-v shooter-data:/root/.shooter \
--restart unless-stopped \
shooter
```
> **Required:** Set `API_KEY` either via `-e API_KEY=...`, `--env-file .env`, or in `docker-compose.yml`. Without it, all authenticated endpoints return 401.
The multi-stage Dockerfile uses `node:20-slim` with native addon binaries copied from the build stage (no build tools in the production image). 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
```yaml
services:
shooter:
build: .
ports:
- '54007:54007'
env_file:
- path: .env
required: false
# Set API_KEY in .env or uncomment below:
# environment:
# - API_KEY=your-secret-key-here
volumes:
- shooter-data:/root/.shooter
restart: unless-stopped
# Optional: Cloudflare Tunnel for remote access
# Start with: docker compose --profile tunnel up -d
tunnel:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate --url http://shooter:54007
depends_on:
- shooter
restart: unless-stopped
profiles:
- tunnel
volumes:
shooter-data:
```
---
## API Reference
All endpoints require the `Authorization: Bearer ` 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` | Stub — returns 501 (not yet implemented) |
| `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
```bash
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:
```json
{
"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
```bash
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 writing
```
**Note:** `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` | Quick setup (~60s): API key + build. `--auto` for non-interactive, `--push` for push config |
| `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](https://github.com/nicktaf/type-crafter). Never edit files in `src/lib/types/generated/` 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/
lib/
types/
generated/ # Auto-generated TypeScript types (DO NOT EDIT)
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
activity/ # Activity feed components
dashboard/ # Dashboard components
neurolink/ # Neurolink integration 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 roadmap
```
---
## Updating
If you installed via the one-command installer:
```bash
cd ~/.shooter/repo
git pull origin release
pnpm install
pnpm build
shooter stop && shooter start -d
```
Or re-run the installer -- it detects the existing installation and offers to update:
```bash
curl -fsSL https://raw.githubusercontent.com/juspay/shooter/release/scripts/install.sh | sh
```
If you installed via npm:
```bash
npm update -g @juspay/shooter
```
---
## Uninstall
```bash
# 1. Stop the server and disable autostart
shooter stop
shooter autostart off
# 2. Remove the data directory (config, logs, SQLite database)
rm -rf ~/.shooter
# 3. Remove the global command symlink
rm -f ~/.local/bin/shooter
# 4. Remove the repo (if installed via one-command installer)
rm -rf ~/.shooter/repo
```
If you installed via npm: `npm uninstall -g @juspay/shooter`
To also remove Claude Code hooks, delete the `hooks` section from `.claude/settings.json` in each project that uses Shooter.
---
## Reset
To reset Shooter to a clean state without reinstalling:
```bash
shooter stop
rm ~/.shooter/.env # Remove config (re-run shooter setup to regenerate)
rm ~/.shooter/shooter.db # Remove terminal history database
rm -rf ~/.shooter/logs # Remove log files
shooter setup # Regenerate config
shooter start -d # Restart
```
---
## Troubleshooting
### 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 build` completed without errors before running `pnpm start` -- `server.ts` has a build guard that exits with a clear error if `build/handler.js` is missing
- Confirm `.env` exists and `API_KEY` is set (the server also checks `~/.shooter/.env` as a fallback)
- On Linux, ensure build tools are installed: `python3`, `make`, `g++` (needed for native modules)
### WebSocket connections fail
- `pnpm dev` does **not** run the WebSocket server. Use `pnpm build && pnpm start` for full functionality.
- Ensure you are obtaining a ticket via `POST /api/ws-ticket` before 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`, and `DEVICE_TOKEN` are 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.json` is in `android/app/` and FCM credentials are in `.env`
- Check `GET /api/debug` for APNs configuration status and device token validity
- Check server logs for APNs or FCM error responses
### Hooks not sending notifications
- The notifier reads `API_KEY` from `~/.shooter/.env` automatically. If that file is missing or empty, run `shooter setup`.
- Verify the hooks are configured in `.claude/settings.json`
- Test connectivity: `curl -H "Authorization: Bearer $(grep API_KEY ~/.shooter/.env | cut -d= -f2 | tr -d '\"')" 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
- `shooter start` detects port conflicts automatically and prints a clear error
- Default port is 54007. Set `PORT=` in `~/.shooter/.env` to use a different port
- To find what's using the port: `lsof -i :54007` (macOS) or `ss -tlnp | grep 54007` (Linux)
---
## Security
- **Command allowlist** -- Only `zsh`, `bash`, `sh`, `fish`, `claude`, and `opencode` can 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 `
- **Working directory validation** -- The `cwd` parameter is validated against the user's home directory; symlink traversal is blocked
- **No credentials in code** -- All secrets loaded from `.env` at runtime; `.env` is gitignored
- **APNs JWT rotation** -- Push notification tokens are generated with short expiry and rotated automatically
---
## License
MIT