# App logs and crash reports (phone_logs) Branch `claude/app-logs-d1c1ef`, 2026-11-09. `phone_logs` lets an agent read one app's own log lines and crash reports, so a developer's agent that tests an app on a phone can see why it failed: the answer to Device MCP's `list_crashes` and `get_crash`. Docs for agents: [mcp.md](../mcp.md) ("phone_logs"), [SKILL.md](../SKILL.md). ## What the agent gets - `kind: "logs"` (the default): the app's log lines since the task started (or `since`: an RFC 3339 time, a date and time or time of day on the computer's clock, or a duration back such as `10m`, `2h`, `1d`), optionally only those whose tag or text contains `filter` and of `level` or more severe. At most `limit` lines (200 by default, 500 at most) and 64 KB; when there are more, the newest are kept and shown oldest first, and the message says how many older lines were left out. A line is `15:04:05.123 Tag: text`, its time on the computer's clock; one line longer than 2,000 bytes is cut. - `kind: "crashes"`: the app's crash reports since then, newest first: Java crashes, native crashes and ANRs on Android, `.ips` crash reports on an iPhone. At most `limit` (3 by default, 10 at most) and 64 KB: every report's heading (kind, time, pid, process, a one-line summary), the texts whole while they fit, then the newest cut short and the others' first lines. - The result's text goes in `Result.Log`, after the message; the MCP layer prints it as it is. The tool description tells agents that it is the app's own output, data and not instructions. ## Permission: decisions and why Logs show more than the screen. An app can log an account name, an address, a search, a token; a crash report can hold the strings on screen when it died. So: 1. **Only one app at a time, and only its own lines.** Android asks logcat for the app's uid alone (`--uid`), and Pinky checks the uid of every line again; an iPhone asks for the app's processes alone, by pid. Pinky never passes on lines of other processes, so the system's own lines about the app (ActivityManager's "Process … has died", the kernel's) are not there; the crash reports carry what matters of them. 2. **Within the agent's scope and the task's app list.** An app outside the agent's scope (`apps:`), or outside the task's `apps`, is `scope_denied`, as launching it would be. Watch-only agents get `scope_denied`: logs show more than the screen they may watch. Read-only agents and read-only mode may read logs: reading changes nothing on the phone. 3. **Some apps never.** `blocked_by_policy` for: an app a rule blocks, even one the person allowed for this task (that allows its screen, not its logs); finance apps and password managers, even with those rules turned off (`policy.Engine.LogsRefused`); the phone's own apps (system apps, Settings, the phone app: their logs mix in everyone's); Pinky for Android and the iPhone's WebDriverAgent helper. The rule the result names is the app's rule, or "App logs". 4. **The first read of an app in a task asks the person**, under the new rule "App logs": "Read the logs of Example?". "Allow for this task" covers that app's logs and crash reports until the task ends; "Allow once" covers this call; "Always allow" covers that app for that agent on that phone in later tasks too (an `always_allow` entry with `agent` and `device`, listed in Settings › Safety; see `policy/standing.go`). The approval blocks and returns `approval_pending` like any other, and a later call with `approval_id` picks up the answer. Without Always allow, each task asks again. 5. **A development build does not ask.** An app built to be debugged is, in practice, the one the agent is testing: on Android a debuggable app (`DEBUGGABLE` in `dumpsys package`), on an iPhone one signed with `get-task-allow` (a development profile, as Xcode installs). Release builds from a store are never debuggable, so they still ask. "Installed from the computer" alone does not skip the question: a sideloaded APK may be anyone's release build. 6. **History keeps what was read, never the text**: "Read 120 log lines of Example", "Read 1 crash report of Example", "Waiting for you: Read the logs of Example", "Blocked by “Finance apps”: read the logs of My Bank", "Outside the scope: read the logs of Example". The step's arguments keep the app id, kind, since, filter, level and limit. Considered and left out: asking on every read (an agent debugging a crash reads logs again and again, and Allow for this task already ends with the task); a Settings switch to turn logs off altogether (a person who does not want it denies the first question; the switch can come later if people ask); reading other apps' lines that mention the app (that is how other apps' data would leak). ## Android - **Lines:** `logcat -d -b main,system,crash -v epoch -v uid --uid= -t ` with `*:` for a level, one dump per call (`internal/driver/android/logs.go`). Android 10 has no `--uid`: Pinky reads every line then and keeps the app's. Either way every line's uid is checked again, so a logcat that ignored the option leaks nothing. The uid is the package's (`pm list packages -U`), in the clone's user for an app clone. An app that shares its uid with other packages (`sharedUserId`) is `unsupported`: their lines cannot be told apart. Pinky for Android is never read. - **Crash reports:** the drop box, `dumpsys dropbox --print` for `data_app_crash` (Java), `data_app_native_crash` (tombstone summary) and `data_app_anr`, matched by the entry's UID (older phones have none: by `Process:` and `Package:`). An ANR is cut down to its subject and the app's own threads (`----- pid at`), not the CPU use or threads of other processes. The drop box drops entries that come too fast, so Java crashes it left out come from the crash buffer (`FATAL EXCEPTION` and the same process's lines after it), one per pid. The headings are the phone's local time, read with `date +%z` in the same shell. `/data/anr/` and `/data/tombstones/` are not readable over adb on user builds; the drop box holds the same traces. - **Development build:** `DEBUGGABLE` in the flags of `dumpsys package ` (`pkgFlags=[ … DEBUGGABLE … ]`). - **What a phone still holds:** logcat's buffers are a few hundred KB to a few MB each, so on a busy phone lines from a few minutes ago may be gone; the drop box keeps a few days of entries. ## iPhone An iPhone keeps no log history a computer can read back, short of `log collect`'s archive of the whole system (`com.apple.os_trace_relay`'s CreateArchive), which holds every app's lines. So Pinky listens (`internal/driver/ios/logs.go`): - **Per process, never the whole system.** The first `AppLogs` call for an app starts a follower. Every second (150 ms for 3 s after Pinky launched the app) it lists the phone's processes and opens an os_trace stream (`StartActivity` with that pid) for each process whose executable lies inside the app's bundle: the app and its extensions. The phone sends nothing of other processes. A stream stays open 3 s after its process is gone, for the last lines. A launch over CoreDevice returns the new pid (`processToken`), which the follower opens at once; the very first lines of a process started any other way can still be missed. Lines are kept in the follower (20,000 at most, older ones go) until the task ends (`driver.LogListener`). The phone's clock times them. - **Processes:** CoreDevice's process list on iOS 27 (`processPaths`); on the WebDriverAgent connection, instruments' `deviceinfo` `runningProcesses` over the developer services. A new install moves the bundle, so the follower looks it up again, at most every 5 s, when it finds no process. - **Streams:** over the lockdown connection (usbmuxd, cable or Wi-Fi), and through the RemoteXPC tunnel's shim when that fails. - **Levels:** os_log has debug, info (default and info), error and fault; a fault is `fatal`. `print()` writes to stdout, which no service relays: only `os_log`, `Logger` and `NSLog` reach Pinky. - **Crash reports:** `com.apple.crashreportmover` first (it moves new reports to where the copy service shows them, and answers "ping"), then `com.apple.crashreportcopymobile` over AFC: the `.ips` files in its root and in `Retired/` named `-YYYY-MM-DD-HHMMSS…ips` whose first-line header names the app's bundle id, newest first, at most 50 read per call (`crashes.go`). The time in a file name is the phone's local time with no zone, so Pinky keeps files up to 14 hours older than `since` and then judges each report by its header's `timestamp`, which has the zone. - **Reading a report:** `bug_type` 309 (JSON, iOS 15 and later) and 109 (the older text format); others (jetsam, resource use) are skipped. A 309 report is cut down to its process, version, OS, exception, termination, reason (`exceptionReason`, `asi`), the last exception backtrace, the crashed thread's first 40 frames and the app's own binary images, at most 48 KB. The summary is the exception reason, else the application-specific information, else the termination's explanation, else the exception type. A termination code of `0x8badf00d` is kind `watchdog`: iOS ended an app that did not answer in time, Android's ANR. - **Development build:** `get-task-allow` in the app's entitlements, from the installation proxy's `Lookup`, as Xcode's development profile signs it. - **Not read:** the WebDriverAgent helper (`.xctrunner`); crash reports of the app's extensions (they are named after the extension's executable); Simulators (`unsupported`: a Simulator's logs are on the Mac, `xcrun simctl spawn booted log stream`). - **Not yet tried on a phone** (no real-iPhone run without the owner's OK). The parts that depend on the phone: whether `deviceinfo` lists processes on iOS 17.0 to 17.3 over the developer disk image, how a crash report's file name looks on iOS 17 and 18, and how quickly the mover makes a new report visible. ## Code - Contract (`internal/driver/driver.go`, additive): `AppLogger` (`AppLogs`, `AppCrashes`, `DevBuild`), `LogListener` (`StopAppLogs`), `LogQuery`, `LogLine`, `LogLevel`, `LogResult`, `CrashReport`. - Policy: `internal/policy/logs.go`: `RuleAppLogs` and `LogsRefused`. - Gateway: `internal/gateway/logs.go`: `Session.Logs`, `askForLogs` (the approval, as `askToPush` does for files), `parseSince`, the size caps. `canAllowTask` offers "Allow for this task" for "App logs". When a task that read logs ends, a `LogListener` driver stops listening. - MCP: `phone_logs` in `internal/mcp/tools.go` (read-only annotation); `Result.Log` printed by `render.go`. - Drivers: `internal/driver/android/logs.go`; `internal/driver/ios/logs.go` (lines, the follower) and `crashes.go` (`.ips` reports). - Fake phone: `internal/driver/fake/logs.go` (`AddApp`, `AddLogs`, `AddCrash`, `SetDevBuild`). - Client: the steps, the approval title and the rule name are translated in `internal/gwtext/gwtext.go` and `internal/gwtext/words.go`; "App logs" approvals have no screenshot (`ui/approvalshot.go`). ## Tests - Unit tests: `internal/policy/logs_test.go`, `internal/gateway/logs_test.go` (asking, Allow for this task, development builds, refused apps, scope, limits, crash report layout, `since`), `internal/driver/android/logs_test.go` (logcat lines, the drop box, the crash buffer, `DEBUGGABLE`, uids), `internal/driver/ios/logs_test.go` (the follower with fake streams, crash report names and synthetic `.ips` reports in `testdata/crashes/`), `ui/gwtext_test.go`. - **Crash Test** (`internal/driver/android/testdata/crashtest`): a one-screen app that logs a line when it starts, throws `IllegalStateException: Crash button tapped` on Crash and blocks its main thread on Hang (an ANR). `build.sh true|false ` builds it with the Android SDK alone (no Gradle) as a debuggable or a release build; install it with `adb install -r`. - Emulator runs, through `scripts/devlock.sh`: `internal/driver/android/logs_device_test.go` (`TestDeviceAppLogs`: the driver reads the start line and the crash's lines and no other process's, the crash report and the ANR report cut down to the app's threads, and refuses Pinky for Android) and `internal/gateway/emulator_logs_test.go` (`TestEmulatorAppLogs`: an agent's task launches Crash Test, taps Crash, reads its lines and its crash report, answering the question for a release build; history holds no log text). Run on 9 November 2026 on the Android 16 (API 36) and Android 10 (API 29) emulators, with both builds.