This is a submission for the Presage SmartSpectra SDK Challenge.
You hand Claude Code a task. It says "Elucidating…" and the spinner starts. For the next two minutes you watch it. Shoulders up, jaw set, breath shallow, eyes locked on a terminal that isn't asking you for anything.
That waiting time is the most common moment in AI-assisted coding, and it's dead time. Exhale turns it into a minute of slow breathing, with a twist: the horizon on screen is your own breathing, read live from an ordinary webcam by the Presage SmartSpectra SDK. No wearable, no chest strap.
What I Built
Exhale is a small desktop app that listens for your coding agent. It's built on Electron and tested on macOS; the SDK also ships Windows and Linux runtimes.
- You send Claude Code a prompt.
- If the run lasts longer than 10 seconds, a small card rises in the corner of your screen. It's a sky at dusk.
- A sun climbs while you breathe in and sinks while you breathe out: 4 seconds in, 6 out, six breaths a minute.
- The horizon the sun sinks behind is your own breathing, from the SDK's chest waveform.
- When the agent finishes, the camera turns off and the card shows a short recap.
Short runs never turn the camera on. In daily use you don't click anything; the first time, the card asks before it uses the camera. It also works with any long command through exhale run -- npm test.
Demo
Every reading in the video comes from one live session with the real SDK. In that run Claude worked for 87 seconds, the guide ran for 77, my breathing settled at 6 to 7.4 breaths a minute, and I was in sync with the guide for 30 of the 51 seconds Exhale could score (59%; scoring starts once the breathing waveform arrives). The narrator voice is AI-generated; the footage and the numbers are not.
Code
github.com/gabrielanhaia/exhale: setup in the README, plus a Docker setup with a simulated sensor so you can try the whole flow without a camera or an API key. The repo has 34 unit and integration tests and 8 Playwright tests that drive the real Electron app.
How It Responds to Human Signals
Exhale requests four metrics from the SDK and nothing else:
| Signal | What Exhale does with it |
|---|---|
CHEST_BREATHING waveform |
Draws the horizon, and scores how closely you follow the guide |
BREATHING_RATE |
Breaths per minute in the recap |
BLINKING |
Suggests a 20-second look-away when you stop blinking |
TALKING |
Pauses the guide while you speak |
Following the guide. Every second, Exhale takes the last 20 seconds of your chest waveform, resamples it to 10 Hz and compares its shape with the guide curve. The score is the best Pearson correlation over a lag of 0 to 2 seconds, because people follow a visual cue a little late. It uses the absolute value, since the SDK's trace is in arbitrary units and Exhale doesn't assume whether an inhale goes up or down. At 60% or above, the card's text turns gold and the sun's glow widens.
Blinks. Rising edges in the blink stream give a rolling blinks-per-minute over 60 seconds. Below 8 a minute, after at least 45 seconds of face time, the card suggests looking at something far away. At most once every five minutes.
Talking. The guide pauses only when most of the last 1.5 seconds of talking detections are confident ones, so a flicker of the mouth doesn't stop it.
The plumbing.
- Claude Code hooks (
UserPromptSubmit,Stop,SessionEnd) POST to a local server on127.0.0.1. The hook skips runs where Claude Code reports the session as unattended (CLAUDE_CODE_SESSION_ATTENDED=0). I checked that a headlessclaude -prun sets it, so scheduled jobs don't turn the camera on. - Each run gets a fresh child process that owns the SDK, with
useCamera()and explicit capture settings (720p at 30 fps). - The child streams small frames over IPC to a session state machine (idle → pending → active → summary). The UI renders from Server-Sent Events.
The SDK hands over protobuf packets. This is the function in src/engine/sdk-engine.js that turns each one into the small frame the rest of the app understands:
function toFrame(m) {
const b = m.breathing || {};
const f = m.face || {};
const sec = (x) => tsToSec(x.timestamp);
return {
trace: (b.upperTrace || []).map((x) => ({
t: sec(x), v: x.value,
})),
rate: (b.rate || []).map((x) => ({
t: sec(x), value: x.value, stable: !!x.stable,
confidence: x.confidence,
})),
blinks: (f.blinking || []).map((x) => ({
t: sec(x), detected: !!x.detected,
})),
talking: (f.talking || []).map((x) => ({
t: sec(x), detected: !!x.detected, stable: !!x.stable,
})),
};
}
Four things the SDK taught me
These came up while testing on real hardware (Node SDK 3.4.0, macOS on Apple Silicon).
-
A second
sdk.on('metrics')replaced the first. I added a debug logger as another metrics listener, and the handler that fed the UI went quiet. No error, just a flat horizon. Keep one handler per event and do everything in it. There's now a regression test for this. -
The metric streams start at different times. In my logs, blink detections arrived from the first packet and the breathing waveform about 20 seconds later; the breathing rate needs a 30-second window on top of that. The card says "Reading your breathing…" until the waveform shows up.
toFramekeeps the SDK's timestamps; the session then lines each batch up by when it arrived, keeping the spacing between samples, instead of assuming every stream shares one clock. - Frame rate matters more than resolution. My 4K webcam, in a fairly dim room, kept falling under the frame rate the SDK needs, and the SDK said so through its validation status. During one of those sessions the native pipeline also stopped with an OpenCV assertion about mismatched image sizes, and the whole app went down with it. Now each run's SDK lives in its own process: a crash ends that run with a clear message, and the app stays up. An iPhone through Continuity Camera, in the same room, gave no frame-rate warnings.
- The talking detector is sensitive. During my sessions the guide kept pausing while I wasn't speaking; small mouth movements were enough to trigger it. That's why the rule above needs a sustained, confident signal.
Prize Categories
- Best Thoughtful Real-World Use: it fits a moment developers already have dozens of times a day, asks before it uses the camera, runs only during that moment, stores nothing by default, and never pretends to be a medical tool.
- Most Creative or Delightful Use: the breathing signal isn't shown as a number on a chart. It becomes the landscape the guide's sun sets behind.
Consent and Data Handling
- Consent. The first time a long run starts, the card explains what will happen and asks before the camera is used. Until you click "Turn on camera", it stays off. You can withdraw consent from the Exhale window at any time, which turns the camera off immediately. On macOS the system camera prompt appears once as well.
- When the camera is on. Only while an agent run is in progress and the card is showing. The card says "camera on" and the menu-bar item shows a dot. "Turn off camera" stops it at once. When the run ends, the camera is released. Unattended runs never trigger it.
- Where video goes. Frames go from the camera into the SmartSpectra SDK inside a process Exhale starts for that run. Presage's README says the SDK computes the vitals on the device. Exhale never writes video or images to disk and never sends them anywhere.
-
What leaves the computer. In my SDK logs, the SDK contacts Presage to check the API key, verify usage and fetch its models. Its optional aggregate telemetry is described in Presage's telemetry docs. Exhale switches it off (
enableTelemetry: false). Exhale has no analytics of its own and doesn't use the SDK's optional LLM Insights. -
What is stored, and for how long. By default one file,
~/.exhale/settings.json: when you gave consent, your history preference and your chosen camera. History is opt-in. When it's on, each run adds one summary line to~/.exhale/history.jsonl(duration, percent with the guide, start and end breathing rate, look-away reminders). No waveforms, no video. Both files are readable only by your user. They stay until you delete them: "Delete all Exhale data" removes both, and switching history off deletes the history file. -
Sharing. None. The local server listens on
127.0.0.1only and rejects requests from web pages (custom header plus Origin check) and from non-loopback host names (DNS-rebinding guard). - Not medical. Exhale is a breathing exercise and a screen-break reminder. It doesn't diagnose, treat or monitor any condition, and it isn't meant for health decisions. It never runs covertly: the camera works only after consent, only during your own agent runs, with a visible indicator.
Agent Session
I built Exhale with Claude Code, in one long session inside Hermes IDE. I'm not embedding the transcript: it contains local machine paths and personal details that I won't publish. The repo history shows how it came together.
If you spend your days waiting on agents too, try it for a week and tell me what your recap says. If you build with agents and want the patterns behind long-running tool loops, my AI Agents Pocket Guide covers them.
- Book: AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs
- Also by me: Thinking in Go (2-book series): Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub, an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub



Top comments (0)