- macOS 15+ (Sequoia) — required for the agent and iOS Simulator
- Xcode 16+ — builds the agent and iOS app
- Go 1.25+ — builds the backend
- PostgreSQL 17+ — database. Run locally or use
docker compose --profile dev up -d postgres-dev - Claude Code — the CLI tool that AFK monitors. Install:
npm install -g @anthropic-ai/claude-code - Docker (optional) — for running the full stack locally via
docker compose
AFK/
backend/ Go server
cmd/server/ Entry point (main.go, route registration)
internal/
auth/ JWT, Apple Sign-In, email/password, WS tickets
config/ Environment variable loading
db/ PostgreSQL queries and migrations
hub/ WebSocket hub (agent + iOS connection pools)
model/ Data types (Session, Event, Device, Command, etc.)
monitor/ Stuck session detector
push/ APNs client and decision engine
internal/db/ PostgreSQL queries and migrations
agent/AFK-Agent/ macOS .app bundle
Session/ SessionWatcher (JSONL polling), EventNormalizer
Network/ WebSocketClient
Security/ E2EEncryption, ContentRedactor, CommandVerifier
Command/ CommandExecutor, CommandValidator
Auth/ KeyAgreementIdentity, DeviceIdentity
Setup/ HookInstaller (PreToolUse/PostToolUse hooks)
Config/ AgentConfig
ios/AFK/ SwiftUI app
Views/ All UI (Auth/, Home/, Sessions/, Settings/, Devices/, NewChat/)
Services/ APIClient, WebSocketService, SessionStore, AuthService, SyncService
Security/ E2EEService, DeviceKeyPair
Model/ Data types, SwiftData models, SessionActivityAttributes
Persistence/ LocalStore (SwiftData)
config/ Shared xcconfig files (gitignored secrets)
nginx/ Reverse proxy config
docs/ Documentation
cd backend
cp .env.example .envEdit .env. The only required variable for local development is AFK_JWT_SECRET:
AFK_JWT_SECRET=$(openssl rand -hex 32)
AFK_LOG_FORMAT=text
AFK_LOG_LEVEL=debugAFK_LOG_FORMAT=text enables human-readable log output instead of JSON.
cd backend
go build ./cmd/server
./serverThe server listens on http://localhost:9847. Verify: curl http://localhost:9847/healthz
The backend requires a running PostgreSQL instance. Set AFK_DATABASE_URL in .env (defaults to postgres://afk:afk@localhost:5432/afk?sslmode=disable). To start a local PostgreSQL via Docker:
docker compose --profile dev up -d postgres-dev
# This exposes PostgreSQL on localhost:5434
# Set AFK_DATABASE_URL=postgres://afk:devpassword@localhost:5434/afk?sslmode=disablecd backend
go test ./... # all tests
go test -race ./... # with race detection
go test ./internal/hub # specific package| Package | Purpose |
|---|---|
github.com/gorilla/websocket |
WebSocket server |
github.com/jackc/pgx/v5 |
PostgreSQL driver (pure Go) |
github.com/golang-jwt/jwt/v5 |
JWT signing/verification |
golang.org/x/crypto |
bcrypt for password hashing |
github.com/joho/godotenv |
.env file loading |
No web framework. Routes are registered directly on http.ServeMux in cmd/server/main.go.
# Copy the example config
cp config/AgentSecrets.xcconfig.example config/AgentSecrets.xcconfigEdit config/AgentSecrets.xcconfig:
AFK_SERVER_URL = https://localhost:9847
For local development without TLS, use http://localhost:9847. The agent auto-derives the WebSocket URL by replacing https:// with wss:// (or http:// with ws://).
Open in Xcode:
open agent/AFK-Agent.xcodeprojOr build from the command line:
cd agent
xcodebuild -scheme AFK-Agent -destination 'platform=macOS' -allowProvisioningUpdates buildThe agent is a macOS .app bundle with LSUIElement=YES — it appears in the menu bar, not the Dock. It needs:
- A running backend to connect to
- Claude Code installed and resolvable via
/usr/bin/which claude - Keychain access for device keys (prompted on first launch)
- On launch, the agent polls
~/.claude/projects/every 2 seconds for modified.jsonlfiles. - New JSONL lines are parsed by
EventNormalizerinto structured events with separatedata(telemetry) andcontent(sensitive text) fields. - Events are sent to the backend over WebSocket as
agent.session.eventmessages. - The agent installs a
PreToolUsehook into Claude Code's~/.claude/settings.jsonthat relays permission requests through a Unix socket at~/.afk-agent/run/agent.sock.
cp config/Secrets.xcconfig.example config/Secrets.xcconfigEdit config/Secrets.xcconfig:
AFK_SERVER_URL = https://localhost:9847
For the Simulator, http:// works. For a physical device, you need a reachable server with valid TLS.
open ios/AFK.xcodeproj
# Select a Simulator target → Cmd+RFrom the command line:
cd ios
xcodebuild build -scheme AFK -destination 'platform=iOS Simulator,name=iPhone 16'For physical devices, update the bundle identifier and signing team in Xcode.
In the Simulator, use email/password sign-in to authenticate. Register a test account via POST /v1/auth/register or the iOS sign-in screen.
See docs/self-hosting.md for the full reference. For local dev, the minimum is:
AFK_JWT_SECRET=<any-hex-string>AFK_SERVER_URL = http://localhost:9847
AFK_SERVER_URL = http://localhost:9847
cd backend && go test ./...xcodebuild test -scheme AFK -destination 'platform=iOS Simulator,name=iPhone 16'xcodebuild test -scheme AFK-Agent -destination 'platform=macOS'- Go:
gofmtformatting.go vet ./...should pass. No web framework — handlers are plainhttp.HandlerFunc. - Swift: Swift API Design Guidelines. Use
actorfor thread-safe state. Use@Observable(notObservableObject) for SwiftUI models. - Xcode projects use
PBXFileSystemSynchronizedRootGroup— adding new Swift files to the filesystem automatically includes them in the build. No manual.pbxprojedits needed. - Commit messages: Conventional Commits (
feat:,fix:,docs:,refactor:,test:,chore:). - No third-party crypto: iOS and agent use Apple CryptoKit exclusively. Backend uses Go's
crypto/*standard library.
End-to-end example: adding a new agent.session.summary message type.
Add the message type constant and payload struct in backend/internal/model/:
// In websocket.go or a new file
const MsgAgentSessionSummary = "agent.session.summary"
type SessionSummaryPayload struct {
SessionID string `json:"sessionId"`
Summary string `json:"summary"`
}In backend/internal/ws/agent_conn.go, add a case to the agent message handler:
case model.MsgAgentSessionSummary:
var p model.SessionSummaryPayload
json.Unmarshal(msg.Payload, &p)
// Store, transform, or relay
hub.BroadcastToUser(userID, msg)In the agent, use MessageEncoder to build the message and send it via the WebSocket client:
// MessageEncoder builds the WSMessage envelope with type + payload + timestamp
let msg = try MessageEncoder.encode(
type: "agent.session.summary",
payload: ["sessionId": sessionId, "summary": summary]
)
try await wsClient.send(msg)In WebSocketService.swift, add a case to the message handler:
case "session.summary":
let summary = payload["summary"] as? String ?? ""
await sessionStore.handleSummary(sessionId: sessionId, summary: summary)In backend/internal/push/decision.go, classify the new event type:
case "session_summary":
return PriorityRoutineExample: adding GET /v1/sessions/{id}/summary.
Create the handler in backend/internal/ (either in an existing handler file or a new one):
func (s *Server) handleGetSessionSummary(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
userID := r.Context().Value(userIDKey).(string)
// Query DB, write JSON response
}In backend/cmd/server/main.go, add the route with the auth middleware:
mux.HandleFunc("GET /v1/sessions/{id}/summary", s.authMiddleware(s.handleGetSessionSummary))In ios/AFK/Services/APIClient.swift:
func getSessionSummary(sessionId: String) async throws -> SessionSummary {
return try await get("/v1/sessions/\(sessionId)/summary")
}- Backend WebSocket traffic: Set
AFK_LOG_LEVEL=debugto see every WebSocket message in the server logs. - Agent logs: Open Console.app on macOS and filter by process
AFK-Agent. The agent logs WebSocket connection state, event processing, and hook activity. - iOS WebSocket: The
DiagnosticsViewin the iOS app (Settings → Diagnostics) shows WebSocket connection status, message counts, and latency. - PostgreSQL inspection:
docker compose exec postgres psql -U afk(prod) ordocker compose exec postgres-dev psql -U afk(dev). - Permission hook debugging: The hook script logs to stderr, visible in the Claude Code terminal. Check
~/.afk-agent/run/for socket and flag files. - E2EE debugging: Both the agent and iOS log key fingerprints on enrollment. Compare them to verify both sides have the correct peer key. Look for "key agreement" and "fingerprint" in logs.