Tail & Analyze iOS Simulator logs with ease β built for AI agents.
xcw is a small Go-based CLI that streams and inspects Xcode iOS Simulator console logs. It was originally built to help AI agents such as Claude and Codex monitor app logs in real-time, but it's just as useful for human developers. Every line of output is emitted as newline-delimited JSON (NDJSON), so agents can process events incrementally without waiting for the stream to finish. Schema versioning and clearly defined event types make it easy to adapt your parser over time.
This is the command you want:
xcw tail -s "iPhone 17 Pro" -a com.example.myappThat's it. This streams logs from your app in real-time.
-sβ Simulator name (runxcw listto see available simulators)-aβ Your app's bundle ID (runxcw appsto list installed apps)
For AI agents: Always start with
xcw tail -s <simulator> -a <bundle_id>. This is the primary command. Only usequery,watch, oranalyzefor specific use cases after you've triedtail.
- Structured NDJSON output β each log event, summary or error is emitted as a JSON object, perfect for incremental consumption.
- Real-time streaming β tail logs from a booted simulator or a specific device with
xcw tail. - Automatic session tracking β detects app relaunches and emits
session_start/session_endevents so AI agents know when the app restarted. - Tail-scoped IDs β every event carries a
tail_id, so agents can correlate only the events from the current tail invocation. - Per-run file rotation β when recording to disk, each app relaunch (or idle rollover) writes to a new file for clean ingestion.
- Agent-ready output β
agent_hints,metadata,reconnect_notice,clear_buffer,cutoff_reached,heartbeatwithlast_seen_timestamp, and a machine-friendly preset. - Historical queries β query past logs with
xcw queryusing relative durations such as--since 5m. - Smart filtering β filter by app bundle ID, log level, regex patterns, field values (
--where), or exclude noise. - Log discovery β use
xcw discoverto understand what subsystems, categories, and processes exist before filtering. - Deduplication β collapse repeated identical messages with
--dedupeto reduce noise. - AI-friendly summaries & pattern detection β periodic summary markers and analysis mode group similar errors and track new vs known patterns.
- Session-based recording & replay β write logs to timestamped files for later analysis and replay them with original timing.
- Persistent monitoring β run
xcw tailin a tmux session to keep logs streaming in the background across terminals. - Self-documenting β ask
xcw help --jsonorxcw examplesto get machine-readable help and curated usage examples.
brew tap vburojevic/tap
brew install xcwYou can also install directly from source using the Go toolchain:
go install github.com/vburojevic/xcw/cmd/xcw@latestClone the repository and run the provided make targets:
git clone https://github.com/vburojevic/xcw.git
cd xcw
make installRun xcw with no arguments to see a quick start guide:
xcwThese commands give you a feel for xcw without any configuration. They work on macOS with at least one iOS Simulator installed.
# all simulators
xcw list
# only booted simulators
xcw list --booted-only
# machine-readable output (NDJSON)
xcw list -f ndjson# apps on the booted simulator
xcw apps
# apps on a specific simulator
xcw apps -s "iPhone 17 Pro"
# NDJSON format
xcw apps -f ndjson# complete documentation as JSON (useful for agents)
xcw help --json
# usage examples for all commands
xcw examples
# examples for a specific command
xcw examples tail
# machine-readable examples
xcw examples --jsonThe tail subcommand streams logs from the iOS Simulator. It automatically picks the single booted simulator when no simulator is specified.
By default, provide an app bundle identifier via -a/--app. For advanced use cases you can omit --app when supplying a raw --predicate, or use --all to explicitly stream unfiltered simulator logs (can be very noisy).
# tail logs from the booted simulator
xcw tail -a com.example.myapp
# tail logs from a named simulator
xcw tail -s "iPhone 17 Pro" -a com.example.myapp
# force a new session if no logs arrive for 60s
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --session-idle 60s
# stop after 5 minutes (emits cutoff_reached in NDJSON)
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --max-duration 5m
# stop after 1000 logs (emits cutoff_reached in NDJSON)
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --max-logs 1000
# machine-friendly preset for agents
xcw --machine-friendly tail -s "iPhone 17 Pro" -a com.example.myapp
# dry-run to see the resolved options as JSON (no streaming)
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --dry-run-json
# filter logs by regex (--filter or --pattern or -p)
xcw tail -a com.example.myapp --filter "error|warning"
# exclude noisy messages (can be repeated)
xcw tail -a com.example.myapp -x heartbeat -x keepalive -x routine
# limit log level range
xcw tail -a com.example.myapp --min-level info --max-level error
# stream in a tmux session so it keeps running in the background
xcw tail -a com.example.myapp --tmux
# write logs to a timestamped file in ~/.xcw/sessions
xcw tail -a com.example.myapp --session-dir ~/.xcw/sessionsxcw provides powerful filtering options for finding exactly the logs you need:
# filter by field with --where (supports =, !=, ~, !~, >=, <=, ^, $)
xcw tail -a com.example.myapp --where level=error
xcw tail -a com.example.myapp --where "message~timeout"
xcw tail -a com.example.myapp --where "subsystem^com.example"
# combine multiple where clauses (AND logic)
xcw tail -a com.example.myapp --where level>=error --where "message~network"
# boolean where expressions (OR/AND/NOT + parentheses)
xcw tail -a com.example.myapp --where '(level=error OR level=fault) AND message~timeout'
# regex literal with flags (case-insensitive)
xcw tail -a com.example.myapp --where 'message~/timeout|crash/i'
# filter by process name
xcw tail -a com.example.myapp --process MyApp --process MyAppExtension
# collapse repeated identical messages
xcw tail -a com.example.myapp --dedupe
xcw tail -a com.example.myapp --dedupe --dedupe-window 5sWhere operators:
| Operator | Meaning | Example |
|---|---|---|
= |
Equals | level=error |
!= |
Not equals | level!=debug |
~ |
Contains (regex) | message~timeout |
!~ |
Not contains | message!~heartbeat |
>= |
Greater or equal (for levels) | level>=error |
<= |
Less or equal (for levels) | level<=info |
^ |
Starts with | subsystem^com.example |
$ |
Ends with | message$failed |
Supported fields: level, subsystem, category, process, message, pid, tid
Use xcw discover to understand what subsystems, categories, and processes are generating logs:
# discover all logs from the last 5 minutes
xcw discover -s "iPhone 17 Pro" --since 5m
# discover logs for a specific app
xcw discover -s "iPhone 17 Pro" -a com.example.myapp --since 10m
# show more results
xcw discover -b --since 1h --top-n 30This is especially useful for AI agents to understand the logging landscape before applying filters.
To capture logs from the very first moment an app launches (including startup logs), use --wait-for-launch:
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --wait-for-launchThis emits a ready event immediately when log capture is active:
{"type":"ready","schemaVersion":1,"timestamp":"...","simulator":"iPhone 17 Pro","udid":"...","app":"com.example.myapp"}AI agent workflow: Start xcw tail --wait-for-launch, wait for the ready event, then trigger your build/run process:
# Terminal 1: Start log capture (emits ready event immediately)
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --wait-for-launch
# Terminal 2: After seeing ready event, build and run
xcodebuild -scheme MyApp build
xcrun simctl install booted MyApp.app
xcrun simctl launch booted com.example.myappIf you see reconnect_notice, there may be log gaps. For NDJSON tails, you can enable best-effort backfill for small gaps:
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --resume --resume-max-gap 2m --resume-limit 2000- Requires
-f ndjson(or--machine-friendly) and--app. - Persists resume state to
~/.xcw/resume/<bundle_id>.json(override with--resume-state). - Emits
gap_detectedand, when backfilled,gap_filled(window is(from_timestamp, to_timestamp]).
xcw query queries historical logs from the iOS Simulator via macOS unified logging (best when you forgot to start tail). It does not read your recorded --output/session files β use xcw analyze / xcw replay for those.
# query the last 5 minutes of logs for your app
xcw query -a com.example.myapp --since 5m
# dry-run to see the resolved query options as JSON (no query)
xcw query -a com.example.myapp --since 5m --dry-run-json
# query with analysis to group and count error patterns
xcw query -a com.example.myapp --since 10m --analyze
# restrict results to errors only
xcw query -a com.example.myapp --since 1h -l error
# persist detected patterns across sessions
xcw query -a com.example.myapp --since 1h --analyze --persist-patternsxcw watch streams logs like tail, and can run commands when it sees errors/faults or message patterns.
# run a command when an error-level log appears (capture command output into trigger_result)
xcw watch -s "iPhone 17 Pro" -a com.example.myapp --where level>=error --on-error "./notify.sh" --trigger-output capture
# run a command when a regex matches the message (pattern:command; can be repeated)
xcw watch -s "iPhone 17 Pro" -a com.example.myapp --on-pattern 'crash|fatal:./notify.sh' --cooldown 10s
# stop after 5 minutes (agent-safe cutoff)
xcw watch -s "iPhone 17 Pro" -a com.example.myapp --where level>=error --on-error "./notify.sh" --max-duration 5m
# dry-run to see the resolved stream options and triggers as JSON (no streaming)
xcw watch -s "iPhone 17 Pro" -a com.example.myapp --where level>=error --on-error "./notify.sh" --dry-run-jsonIn NDJSON mode, trigger executions are correlated by trigger_id (and scoped by tail_id/session):
trigger: emitted when a trigger startstrigger_result: emitted when it completes (exit_code,duration_ms,timed_out, optionaloutput/error)trigger_error: emitted only on failures (sametrigger_id)
Trigger output modes:
discard(default): do not capture stdout/stderrcapture: capture combined stdout/stderr intotrigger_result.output(truncated)inherit: stream trigger output to stdout/stderr (can break NDJSON if stdout is used)
xcw tail uses macOS unified logging, which captures Logger, os_log, and NSLog calls. Swift print() statements go to stdout and are not captured by unified logging.
To capture print() output, use xcw launch:
# launch app and capture stdout/stderr
xcw launch -s "iPhone 17 Pro" -a com.example.myapp
# terminate any existing instance first
xcw launch -s "iPhone 17 Pro" -a com.example.myapp --terminate-existingOutput format:
{"type":"console","schemaVersion":1,"timestamp":"2024-01-15T10:30:45Z","stream":"stdout","message":"Hello from print()","process":"com.example.myapp"}Recommendation: For best results with xcw, use Swift's Logger API instead of print():
import OSLog
let logger = Logger(subsystem: "com.example.myapp", category: "general")
logger.info("This message appears in xcw tail")Logger provides log levels, subsystem filtering, and persistence β all accessible via xcw tail and xcw query.
# record a session to an NDJSON file
xcw tail -a com.example.myapp --output session.ndjson
# analyze a recorded file
xcw analyze session.ndjson
# replay a recorded session with original timing
xcw replay session.ndjson --realtime
# replay at 2x speed
xcw replay session.ndjson --realtime --speed 2.0xcw reads settings in this order (highest wins): CLI flags β environment variables β config file β built-in defaults. This keeps AI agents predictable when they reuse the same tail session across relaunches.
- Environment: prefix every key with
XCW_. Common shortcuts:XCW_FORMAT,XCW_LEVEL,XCW_QUIET,XCW_VERBOSE,XCW_APP,XCW_SIMULATOR. Nested keys work too:XCW_TAIL_HEARTBEAT=2s,XCW_QUERY_LIMIT=200,XCW_WATCH_COOLDOWN=1s. - Config file locations (first found is used):
./.xcw.yaml/./.xcw.yml/./xcw.yaml/./xcw.yml, then~/.xcw.yaml/~/.xcw.yml, then~/.config/xcw/config.yaml(or$XDG_CONFIG_HOME/xcw/config.yaml), then/etc/xcw/config.yaml. - Per-command defaults: set sticky values without repeating flags:
format: ndjson
level: debug
quiet: false
verbose: false
defaults:
simulator: "iPhone 17 Pro"
app: com.example.myapp
buffer_size: 200
since: 5m
limit: 2000
tail:
heartbeat: 5s
summary_interval: 20s
session_idle: 60s
query:
since: 15m
limit: 1500
watch:
cooldown: 2sTip for agents: set
XCW_SIMULATOR="iPhone 17 Pro"andXCW_APP=<bundle>once, then rely on config defaults so a relaunch is treated as the same tail session while still emittingsession_start/session_endmarkers for each new app PID.
Use the --tmux flag with tail to keep logs streaming while you do other work. xcw will print a JSON object containing the session name. Attach to the session at any time using the provided command.
# start streaming in a tmux session
xcw tail -a com.example.myapp --tmux
# the NDJSON output includes:
# {"type":"tmux","session":"xcw-iphone-17-pro","attach":"tmux attach -t xcw-iphone-17-pro"}
# attach later to view live logs
tmux attach -t xcw-iphone-17-pro
# clear the tmux pane between test runs
xcw clear --session xcw-iphone-17-proxcw names session files using the pattern YYYYMMDD-HHMMSS-<prefix>.ndjson (prefix defaults to the app bundle ID). Sessions are stored in ~/.xcw/sessions by default. Use the sessions command to list, show or clean these files:
# list recent session files (sorted by date)
xcw sessions list
# show the path of the most recent session
xcw sessions show --latest
# delete old sessions, keeping only the latest 5
xcw sessions clean --keep 5Primary command: xcw tail β AI agents should use tail for real-time log streaming. This is the main command for monitoring app behavior.
Choosing an output mode:
| Mode | Command | Best for |
|---|---|---|
| Stdout | xcw tail -a APP |
Script agents (Codex) that process NDJSON line-by-line |
| File | xcw tail -a APP --session-dir ~/.xcw/sessions |
Recording logs for later analysis with xcw analyze |
| Tmux | xcw tail -a APP --tmux |
Humans watching logs visually in a terminal |
Recommended AI agent workflow:
# 1. Start recording logs to a session file
xcw tail -s "iPhone 17 Pro" -a com.example.myapp --session-dir ~/.xcw/sessions
# 2. After the test run, analyze the session
xcw analyze $(xcw sessions show --latest)When to use xcw query:
query reads from macOS unified logging (system logs), not from your recorded sessions. Use it only when you forgot to start tail and need to check what happened in the last few minutes:
# Check system logs from the last 5 minutes (not session files)
xcw query -s "iPhone 17 Pro" -a com.example.myapp --since 5m -l errorNote: --tmux is designed for human visual monitoring. AI agents should prefer --session-dir or --output for programmatic access to recorded logs.
xcw automatically detects when your iOS app is relaunched from Xcode. When the app's PID changes, xcw emits session events so AI agents know they're looking at a fresh app instance without needing to restart tailing.
How it works:
- On first log,
xcwemits asession_startevent withsession: 1 - All log entries include a
sessionfield matching the current session number - When the app relaunches (PID changes),
xcwemits:session_endwith summary of the previous session (logs, errors, faults, duration)session_startwithalert: "APP_RELAUNCHED"and the new session number
- When the stream stops, a final
session_endis emitted so the last run is closed. - Optional:
--session-idle 60swill force asession_end/session_startif no logs arrive for 60 seconds (useful to bracket manual test runs).
Example session events:
{"type":"session_end","schemaVersion":1,"tail_id":"tail-abc","session":1,"pid":12345,"summary":{"total_logs":142,"errors":3,"faults":0,"duration_seconds":45}}
{"type":"session_start","schemaVersion":1,"tail_id":"tail-abc","alert":"APP_RELAUNCHED","session":2,"pid":67890,"previous_pid":12345,"app":"com.example.myapp","simulator":"iPhone 17 Pro","udid":"...","version":"1.4.0","build":"2201","binary_uuid":"C0FFEE...","timestamp":"2024-01-15T10:30:45Z"}Stderr alert (for AI agents scanning stderr):
[XCW] π NEW SESSION: App relaunched (PID: 67890) - Previous: 142 logs, 3 errors
In tmux mode, a visual separator banner is written when a new session starts:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π SESSION 2: com.example.myapp (PID: 67890)
Previous: 142 logs, 3 errors | 2024-01-15 10:30:45
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
This allows AI agents to keep xcw tail running continuously while you rebuild and relaunch your app from Xcodeβno need to restart tailing.
Recording to files: When you use --output or --session-dir, xcw now rotates to a fresh file on every app relaunch or idle rollover (one file per run). Filenames include the session number when you provide --output, or a new timestamped file is created when using --session-dir.
Agent contract (do this!):
- Track the latest
session_startand only process logs whosesessionequals that value. - Also require
tail_idto match the current tail invocation; drop events from other tails. - On
session_start,session_end, orclear_buffer, reset any caches (dedupe/pattern memory) before continuing. - When recording to disk, read only the newest rotated file unless explicitly comparing runs.
- Use older sessions/files only when you are asked to compare behavior across runs.
- Watch for
reconnect_notice(andgap_detected/gap_filledwhen--resumeis enabled) to mark possible log gaps; watchcutoff_reachedto know the stream ended intentionally. - Use
metadataat startup for version/commit info;heartbeat.last_seen_timestampto detect stalls.
By default xcw writes NDJSON to stdout. Each event includes a type and schemaVersion field. Common types include log, metadata, ready, heartbeat, stats, summary, analysis, session_start, session_end, clear_buffer, reconnect_notice, gap_detected, gap_filled, cutoff_reached, trigger, trigger_result, trigger_error, console, simulator, app, doctor, pick, and session. The current schema version is 1.
Example log entry:
{"type":"log","schemaVersion":1,"tail_id":"tail-abc","timestamp":"2024-01-15T10:30:45.123Z","level":"Error","process":"MyApp","pid":1234,"subsystem":"com.example.myapp","category":"network","message":"Connection failed","session":1}Example summary marker:
{"type":"summary","schemaVersion":1,"tail_id":"tail-abc","windowStart":"2024-01-15T10:25:00Z","windowEnd":"2024-01-15T10:30:00Z","totalCount":150,"errorCount":4,"faultCount":1,"hasErrors":true,"hasFaults":true,"errorRate":0.8}You can generate a machine-readable JSON schema for validation:
# all types
xcw schema > xcw-schema.json
# specific types only
xcw schema --type log,error,summary
# the canonical schema file lives in this repo at schemas/generated.schema.jsonIf you see errors like βmultiple booted simulatorsβ, either:
- Specify a simulator explicitly:
xcw tail -s "iPhone 17 Pro" -a com.example.myapp - Or pick one interactively:
xcw pick simulator - Or shut down the extra simulators:
xcrun simctl shutdown <udid>
xcw relies on xcrun simctl and log stream. If xcrun fails:
- Ensure Xcode and Command Line Tools are installed (
xcode-select -p). - Open Xcode once after updating to accept the license.
- Try
xcw doctorfor a quick environment check and actionable hints.
- Verify the bundle ID:
xcw apps(then use-a <bundle_id>). - Use
xcw discover --since 5mto learn valid subsystems/categories/processes before filtering. - Remember:
print()doesnβt show up inxcw tail(usexcw launchto capture stdout/stderr).
Shell quoting bites. Prefer quoting complex predicates/expressions:
xcw tail --where '(level=error OR level=fault) AND message~timeout'xcw tail --where 'message~/timeout|crash/i'
If you see reconnect_notice, there may be log gaps. Run with -v/--verbose to surface diagnostics (including xcrun stderr) and watch heartbeat.last_seen_timestamp to detect stalls.
For NDJSON tails with --resume (requires --app), xcw will emit gap_detected and may emit gap_filled after backfilling via query (bounded by --resume-max-gap and --resume-limit).
The following flags apply to all commands:
| Flag | Purpose |
|---|---|
-f, --format <ndjson|text> |
Output format (defaults to NDJSON) |
-l, --level <debug|info|default|error|fault> |
Minimum log level to emit |
-q, --quiet |
Suppress non-log output |
-v, --verbose |
Show debug information (predicate evaluation, reconnection notices) |
xcw was built with AI consumption in mind.
Start here: Use xcw tail to stream logs. Record to a file with --session-dir for later analysis.
Key properties:
- Primary command is
tailβ stream logs in real-time; use--session-dirto record for analysis. - Structured NDJSON output β easy to parse incrementally, one JSON object per line.
- Schema versioning β every record contains a
schemaVersionso agents can handle future changes. - Automatic session tracking β detects app relaunches via PID changes and emits
session_start/session_endevents with summaries. No need to restart tailing when rebuilding from Xcode. - Session recording β capture logs to timestamped files, analyze later with
xcw analyze. - Pattern detection β analysis mode groups similar errors and tracks new vs known patterns.
- Non-interactive β all commands accept flags; no interactive prompts required.
- Self-documenting β run
xcw help --jsonfor complete machine-readable documentation. - Self-diagnostics β
xcw doctorchecks your environment and prints a diagnostics report.
- macOS 14 (Sonoma) or later
- Xcode with the iOS Simulator installed
- Physical iOS devices are not supported yet; Apple doesn't provide a stable CLI for unified logs. Use Console.app or
idevicesyslogas a workaround. tmux(optional, required only if you use--tmuxsessions)
xcw is licensed under the MIT License. See the LICENSE file for details.