# WebTerm iOS Client Native iOS client for the web-terminal server in the repository root. It is a **pure remote client**: it speaks the same WebSocket wire protocol and HTTP endpoints as the web frontend and requires **zero server changes** in P0. ## Build & Run Requirements: macOS 15, Xcode 16.3 (Swift 6.1), [XcodeGen](https://github.com/yonaskolb/XcodeGen), iOS 18.4 simulator (iPhone 16). SwiftTerm 1.13 is resolved for the App target only. ```bash cd ios xcodegen generate # project.yml → WebTerm.xcodeproj open WebTerm.xcodeproj # or run the full test suite headless: xcodebuild -project WebTerm.xcodeproj -scheme WebTerm \ -destination 'platform=iOS Simulator,name=iPhone 16,arch=arm64' test ``` Layout: | Path | What | |---|---| | `App/` | SwiftUI app glue + `WebTermTests` unit-test bundle | | `Packages/WireProtocol` | Frozen wire contract (frames, validation, tunables) | | `Packages/SessionCore` | `SessionEngine` actor: connect/replay/reconnect/gate | | `Packages/HostRegistry` | Paired hosts + last-session persistence | | `Packages/APIClient` | HTTP endpoints (guarded routes carry `Origin`) | | `Packages/TestSupport` | `FakeTransport` / `FakeClock` / `FakeHTTPTransport` | | `IntegrationTests/` | Tests against a real Node server (`npm start`) | Per-package tests: `swift test --package-path Packages/`. The server runs from the repo root: `npm install && npm start` (see the root [README](../README.md)). ## ntfy notification bridge (P0 "host finds the phone") Until APNs push lands (P1, T-iOS-20/21), the phone is notified via the **existing** [ntfy](https://ntfy.sh) bridge that already ships with `npm run setup-hooks`. **No new code** — this chapter documents and verifies the shipped behavior. ### What it does When Claude Code (running inside a web-terminal session on the host) fires a hook event, an extra `curl` posts to your ntfy topic: | Claude Code hook event | ntfy `Priority` header | Meaning | |---|---|---| | `PermissionRequest` (held gate) | `high` | **NEEDS-INPUT** — Claude is waiting for approval | | `Stop` / `SessionEnd` | `low` | **DONE** — the task finished / session ended | Install logic: `scripts/setup-hooks.mjs:229-238` (high on `PermissionRequest` at `:231-233`, low on `Stop`/`SessionEnd` at `:235-237`). The command itself is built by `buildNtfyCommand` (`scripts/setup-hooks.mjs:91-101`). ### Default-off: env unset ⇒ zero side effects The bridge is **opt-in at install time**: `npm run setup-hooks` only adds the ntfy hooks when **both** `WEBTERM_NTFY_URL` and `WEBTERM_NTFY_TOPIC` are set in the environment (gate at `scripts/setup-hooks.mjs:262-264`, guarded install at `:229`). With the vars unset, the written `settings.json` contains no ntfy entry at all (verified: fresh install without env → `grep -c WEBTERM_NTFY settings.json` = 0, and the install log has no ntfy line). Two further layers keep it inert outside web-terminal: - Every installed ntfy command starts with `[ -n "$WEBTERM_HOOK_URL" ] && …` (`scripts/setup-hooks.mjs:93`) — in a normal terminal that var is unset, so the hook is a no-op. - The session env is the only carrier: the server forwards `WEBTERM_NTFY_*` into each spawned session via the `...process.env` spread (`src/session/session.ts:95-102`, comment at `:99-100`). Nothing is hardcoded server-side. ### Setup 1. **Install the ntfy app** on the iPhone (App Store) and allow notifications. 2. **Generate a random topic and subscribe to it.** On the public `ntfy.sh` server, **the topic name is the password**: anyone who knows it can read your notifications and publish to the channel. Use a random string, e.g. `openssl rand -hex 12`. 3. **Export the env vars, then install the hooks and start the server from that same shell** (the install gate reads env at `setup-hooks.mjs:262-264`; at runtime the curl expands `$WEBTERM_NTFY_*` from the session env, which inherits the server's env — `src/session/session.ts:95-102`): ```bash export WEBTERM_NTFY_URL=https://ntfy.sh # or your self-hosted ntfy export WEBTERM_NTFY_TOPIC= # optional, self-hosted/paid access control only: export WEBTERM_NTFY_TOKEN=tk_... # env only — never a literal npm run setup-hooks # expect: Installed ntfy bridge (NEEDS-INPUT=high, DONE=low) → https://ntfy.sh/ npm start ``` Confirmation log line: `scripts/setup-hooks.mjs:296-298`. 4. Restart any running `claude` sessions (the installer prints this reminder). ### What the notification contains (payload minimization) Verified against the shipped command (`scripts/setup-hooks.mjs:91-101`): the curl sends an **empty POST body** — there is no `-d`/`--data` flag at all. The only information that leaves the host is: - **which topic** was posted to (your private random channel), and - the **`Priority` header** (`high` = needs input, `low` = done). No session id, no `cwd`, no command content, no tool names — strictly less than even a "sessionId prefix + status word" payload. In the ntfy app both signals show ntfy's default message text; you tell them apart by the priority rendering (high-priority notifications are visually marked and can bypass quiet delivery). The token is referenced as `Bearer $WEBTERM_NTFY_TOKEN` (`scripts/setup-hooks.mjs:95`) — a shell variable expanded at hook-fire time. It is **never written into `settings.json` or any command literal** (SEC-C6; regression-tested by `test/setup-hooks.test.ts:410-421`, re-run for this verification: 49/49 passed). ### What it does NOT cover: STUCK `stuck` is a **server-derived** state: `manager.sweepStuck` (`src/session/manager.ts:268`) infers it from output silence while a session looks busy. **No Claude Code hook event fires for it**, so a hook-side bridge physically cannot send a STUCK notification. This gap is closed in P1 by APNs (T-iOS-20), which pushes from the server's own event bus. ### Turning it off / P1 supersession Once P1 APNs lands (T-iOS-20/21), this bridge is superseded and can be disabled. Any of: - Re-run `npm run setup-hooks` **without** the `WEBTERM_NTFY_*` env vars — the reinstall pass strips previously installed ntfy groups (marker cleanup, `scripts/setup-hooks.mjs:185-205`; the ntfy command contains the `WEBTERM_HOOK_URL` marker via its guard at `:93`, verified: reinstall without env → 0 ntfy entries remain). - `npm run setup-hooks -- --remove` — removes all web-terminal hooks. ### End-to-end phone check — DEFERRED(需真机/用户操作) Not runnable in this environment (requires the user's phone and mutating the user's live Claude Code hook config). Exact manual steps: 1. iPhone: install ntfy, subscribe to `` on `https://ntfy.sh`, allow notifications. 2. Mac: `export WEBTERM_NTFY_URL=https://ntfy.sh WEBTERM_NTFY_TOPIC=`, run `npm run setup-hooks`, confirm the log line `Installed ntfy bridge (NEEDS-INPUT=high, DONE=low)`, then `npm start` from the same shell. 3. Open the web terminal, start a session, run `claude`, and give it a task that triggers a permission gate (e.g. a Bash command that is not pre-allowed). 4. When the gate is held (tab badge shows waiting-for-approval), the iPhone should receive a **high-priority** ntfy notification within seconds. 5. Approve and let the task finish — a **low-priority** notification arrives on `Stop`/`SessionEnd`.