Documentation
Version 0.3.0 · Changelog
Get started (Mac app)
Requirements: macOS 14+ and Xcode (it brings the iOS Simulator and LLDB). The scan engine is inside the app and runs with the system python3; nothing else to install.
Open WatchMemory → Settings → System check. Every item should be green. If “Developer mode is off”, run the command below once so macOS stops asking for a password each time the debugger attaches.
Run your app's debug build in the Simulator, pick the simulator and the app in the toolbar, then start a residue check.
sudo DevToolsSecurity -enableResidue check: prove logout works
1. Log in and open the screens that handle secrets. 2. Capture “before”. 3. Log out the way a user would. 4. Capture “after”. WatchMemory compares the two and returns PASS, FAIL or INCOMPLETE.
The same flow from the command line. --track-from makes the second capture look again, by value, for the secrets seen before logout, including values no pattern recognises (stored as written; re-encoded copies such as base64 are not followed):
watchmemory residue capture --process MyApp --bundle-id com.acme.app --label "logged in" --out before.json
# log out in the Simulator (UI test, deep link…)
watchmemory residue capture --process MyApp --bundle-id com.acme.app \
--track-from before.json --label "after logout" --out after.json
watchmemory residue compare before.json after.json --html residue.htmlCapture both sides with the same version: comparing a 0.2.x capture with a 0.3.0 capture is refused.
Reading the result
Results are grouped per secret. The same token in memory and on disk is one group, listed with every place it survived, the store and key that hold it (for example SDK:redux-persist › persist:root › auth › refreshToken, the redux-persist root in AsyncStorage) and a fix. The headline reads like “3 secrets survived logout in 7 places”.
Survived: present before and after logout. Appeared: new after logout. Released: gone.
What fails the gate: a canary that survives on disk or in native memory; any survivor on disk at or above --fail-on (default high); a memory-only survivor in native code when MallocScribble was on. A memory-only survivor without MallocScribble, or in a React Native or Flutter heap, is reported with the reason and doesn't fail the gate.
INCOMPLETE (exit 2): a capture read less than the minimum share of writable memory (default 90 %, --min-coverage), a source failed, a file or folder could not be read, a limit was reached, or the before capture found no secrets. A partial scan is never a PASS.
Refresh, access, ID, session and auth tokens are rated High; canaries are Critical.
Canaries: zero false positives
In debug and UI-test builds, let the app create a fake token and use it instead of the real one. Add the WatchMemoryDebugAgent Swift package (DEBUG only) and call WatchMemory.canaryIfRequested(): it returns a fresh WMCANARY_… value when the app was launched in canary mode, and nil otherwise.
Never pass a canary value in launch arguments or the environment. Those stay in memory for the whole process lifetime, so the canary would always “survive”; WatchMemory refuses to scan such a process (exit 2).
Launch with MallocScribble so freed native memory is overwritten and only live copies count:
SIMCTL_CHILD_WM_CANARY_MODE=1 SIMCTL_CHILD_MallocScribble=1 SIMCTL_CHILD_MallocNanoZone=0 \
xcrun simctl launch booted com.acme.app
// Swift, DEBUG builds (WatchMemoryDebugAgent)
let accessToken = WatchMemory.canaryIfRequested("access_token") ?? realAccessToken
// XCUITest
app.launchArguments += ["-WMCanaryMode", "1"]In an Xcode scheme, set WM_CANARY_MODE=1 under Environment Variables and enable Malloc Scribble under Diagnostics.
CI release gate
Install the versioned CLI and check its SHA-256 before you unpack it. Pin the checksum from the release notes in your pipeline instead of trusting the download alone.
V=0.3.0
curl -fsSLO https://watchmemory.app/downloads/watchmemory-cli-$V.tar.gz
echo "$WM_CLI_SHA256 watchmemory-cli-$V.tar.gz" | shasum -a 256 -c - # Linux: sha256sum -c -
tar xzf watchmemory-cli-$V.tar.gz
./watchmemory-cli/bin/watchmemory doctor
./watchmemory-cli/bin/watchmemory ci --process MyApp --bundle-id com.acme.app \
--scan-profile full --baseline .watchmemory-baseline.json \
--sarif wm.sarif --junit wm.xml --html wm.htmlExit codes: 0 PASS, 1 FAIL (policy violation), 2 INCOMPLETE or error (a partial scan, coverage below the minimum, target not found, a canary passed in launch arguments).
Set WATCHMEMORY_LICENSE (Pro or Team) and WATCHMEMORY_HASH_KEY (any long random string; keeps baselines stable) as CI secrets. Without a licence the run is report-only: the verdict in the reports stays truthful and a FAIL still exits 0. Pro enforces for one app; Team for every developer and runner of an app.
--show-values is refused on CI runners (CI, GITHUB_ACTIONS, GITLAB_CI, TF_BUILD, JENKINS_URL, BUILDKITE, BITRISE_IO, CIRCLECI, TEAMCITY_VERSION, CODEBUILD_BUILD_ID and other CI variables listed in docs/CI.md). A GitHub Action plus fastlane and Bitrise examples ship in the integrations folder of the CLI tarball.
Upload SARIF only for a PASS or FAIL: an INCOMPLETE run's SARIF lists only what could be read (it is marked executionSuccessful: false), and code scanning would close the alerts missing from it.
Android (beta)
Requirements: a debuggable build (a debug variant), adb on Linux or macOS, and Python 3.9+. No root. WatchMemory copies the app's private data over adb run-as and scans it on your machine.
adb devices # emulator or USB device with USB debugging
watchmemory residue capture --android-package com.acme.app --label "logged in" --out before.json
# log out in the app (instrumented test, Maestro or by hand)
watchmemory residue capture --android-package com.acme.app \
--track-from before.json --label "after logout" --out after.json
watchmemory residue compare before.json after.json --html residue.htmlScanned: SharedPreferences, DataStore, Room/SQLite (with the -wal file), WebView cookies and storage, the OkHttp cache, MMKV and other files in the app's data directory. Use --adb-serial to pick a device and --android-user for a work profile or secondary user. The same flags work with disk-scan, ci, assert and residue capture (not scan: Android support is disk only).
Limits: disk only (no memory or Java-heap results yet), debuggable builds only (release builds are refused with a clear error), CLI only. Devices from some vendors restrict run-as; emulators are the reference.
React Native, Flutter and KMP
They work on the iOS Simulator with debug builds; Flutter runs only in debug (JIT) mode on the Simulator. Every capture states the runtime it detected (react-native, flutter, kmp or native) and the evidence, such as hermes.framework.
Storage findings name the store and key, such as SDK:redux-persist › persist:root › auth › refreshToken (redux-persist in AsyncStorage), MMKV or flutter.session in shared_preferences. They gate as usual.
Memory-only hits in the JavaScript (Hermes) or Dart heap are report-only. Those heaps are garbage-collected: a freed string stays until the collector reuses the page and MallocScribble doesn't reach it, so failing on it would fail every run. Fix storage first.
Use a development build (npx expo run:ios, flutter run), not Expo Go: Expo Go is a different app with its own data.
MCP server for AI coding agents
Claude Code, Cursor and other MCP clients can call check_environment, list_simulator_processes, memory_usage, scan_memory, scan_disk, compare_residue_captures and assert_no_residue. Raw values are never sent to the model, only masked previews and hashes.
claude mcp add watchmemory -- /Applications/WatchMemory.app/Contents/Resources/Engine/bin/watchmemory-mcpOWASP MAS v2 mapping
Every residue survivor: MASWE-0024 (sensitive data accessible after session termination), MASVS-AUTH-3.
Secrets stored on disk: MASVS-STORAGE-1 with MASWE-0001 or MASWE-0002; test MASTG-TEST-0302 on iOS and MASTG-TEST-0207 on Android.
Memory: MASVS-STORAGE-2. OWASP has no current memory test, and WatchMemory doesn't cite the deprecated MASTG-TEST-0060 or MASTG-TEST-0052.
WatchMemory automates the MASWE-0024 check and gives you evidence. It doesn't certify anything: OWASP doesn't certify apps or tools.
What the numbers mean, and limits
Coverage: the share of the app's writable memory (heap, stack, data; not code or read-only mappings) that was read. Snapshot stops after about 10 s; Full scan and CI read everything.
Values never appear in reports unless you pass --show-values. Hashes are HMACs with a per-install key (or WATCHMEMORY_HASH_KEY), so they can't be reversed by brute force.
Not inspected: release and App Store builds (by design), physical iPhones (planned), the Keychain (planned; delete Keychain items in your logout code), Android memory. Very short NSStrings (up to about 11 characters, such as a PIN) can live inside tagged pointers and are invisible to any memory scan.