---
title: "When it does not work"
---

> Documentation Index
> Fetch the complete documentation index at: https://jasonm4130-labs.github.io/ambient/llms.txt
> Use this file to discover all available pages before exploring further.

# When it does not work

# When it does not work

## Fixes shown in the Welcome page

The Welcome page lists failing doctor checks and keeps Settings available.
After making a change, choose **Check again**.

- Run ./fetch-models.sh from the Ambient checkout to install the models.
- Open Settings and correct the configuration, then check again.
- Choose a writable sessions folder in Settings → Export.
- Quit Ambient, remove transcribing.lock from the listed session folder, then reopen Ambient.
- Open System Settings → Privacy & Security and allow Ambient to use the microphone and system audio.
- Run ambient doctor in a terminal to inspect the failure, then check again.

Almost every first-run failure here is the same one wearing different clothes:
the recording completes, reports success, and contains silence. macOS does not
warn you, because from its point of view nothing went wrong.

If your call track is flat, work through this page before suspecting the
recogniser or your microphone.

## Start with doctor

```sh
ambient doctor
```

It names which of ten things — model files, config, sessions directory
state — is actually missing, so it is cheaper than reading the rest of this
page. See the [verb reference](commands.md) for what each check covers.

## It recorded silence

The two services then diverge, which is what makes this so confusing:

| Service | csreq matches | Behaviour |
|---|---|---|
| `kTCCServiceMicrophone` | no | keeps working |
| `kTCCServiceAudioCapture` | no | **runs, returns zeros, no error, no prompt** |

These are two independent failures and it is easy to conflate them:

| Launch | Signing | Result |
|---|---|---|
| shell (`build/…/MacOS/ambient`) | anything | **silent** — TCC blames the terminal |
| `open -a` | stale cdhash | **hangs forever** on a prompt nobody sees |
| `open -a` | stable identity | works |

The first row holds regardless of how the app is signed, so fixing the
certificate does not make a terminal launch work — measured after the fix:
direct launch still gave `call peak 0.000`, `open -a` gave `0.869`.

`./setup-signing.sh` fixes this permanently by signing with a certificate, so
the requirement becomes `identifier "uk.ambient.cli" and certificate leaf = …`
and survives rebuilds. `ambient tap` and `ambient record` now detect the silent
case at runtime — they sample `kAudioProcessPropertyIsRunningOutput` during
capture and, if the call track is flat while something was demonstrably playing,
name the likely cause — checking the launch method first (parent pid 1 means
LaunchServices, anything else means a shell), and only then the cdhash.

On a managed Mac a PPPC profile denying `kTCCServiceAudioCapture` produces
**identical** symptoms, so rule the cdhash out before blaming IT.

See [ADR-0001](../adr/0001-core-audio-process-tap.md) and [ADR-0002](../adr/0002-signed-bundle-launch.md).

## macOS asked and I clicked Don't Allow

A denial adds no fourth symptom. The tables above sort the ways a grant goes
missing; refusing the prompt is another way, and it produces the same recording
as the others — tap created, buffers at the right rate, every sample zero.

The binary does diagnose it, in `silent_tap_advice` in `src/capture.rs`, which
both `tap` and `record` call once the capture is over. When the call track is
flat while something was demonstrably rendering, it checks the parent pid — 1
means LaunchServices, anything else means a shell — and on the LaunchServices
branch it prints the paragraph written for exactly this case: what you are
looking at is a denied system-audio tap, Core Audio returns success, delivers
correctly-shaped buffers, and fills them with zeros, reporting no error and
prompting for nothing. It then blames the cdhash first, because a grant
orphaned by a rebuild is the commoner cause, and offers three things to do:
`codesign -dvvv build/Ambient.app 2>&1 | grep CDHash` to see the hash the app
now has, a `sqlite3` query against `~/Library/Application
Support/com.apple.TCC/TCC.db` for the `csreq` TCC expects, and, if the two
differ, either signing with a stable identity or

```sh
tccutil reset AudioCapture uk.ambient.cli
```

That last command is the one that clears a denial: it removes the row, so the
next bundle launch asks again. `./setup-signing.sh` runs the same command as its
final step, so re-running it clears a denial as a side effect of re-signing.
Neither has been exercised here against a row that was actually denied — the
cdhash mismatch they were written for is the case that was.

Two things about that advice are worth knowing before you go looking for it.
The `sqlite3` query selects `service` and `csreq` and not the field recording
whether the row allows or refuses, so it cannot tell a denial from a stale
identity; both end at the same `tccutil` line anyway. And the paragraph is
reached only when the tap was unfiltered. With `apps` set, an earlier branch
answers first and reports that those apps played no audio during the recording,
which is what a denied grant also looks like from inside the process.

The reason this page carries the advice at all is that nobody could read it
where it was printed. It goes to stderr, and the branch that produces it runs
only when LaunchServices started the process — the launch with nowhere for
stderr to go. Two things now catch it anyway. The capture warnings — this one
and the room-silent warning above it — are written into the session's own
`session.json`, in a `warnings` array, and the window shows them in a banner
over the transcript, so the news reaches the place you go to read the
recording. And the menu bar keeps its own log at
`~/Library/Logs/Ambient/app.log`; only what it routes through `log` lands
there, and a recording that completes is logged twice: `audio written: <dir>`
when the wav files are closed, then `transcript written: <dir>` when the
transcript is exported. Both lines appear whether the call track held anything
or not.

A session recorded before the `warnings` array existed simply has none, and
parses as it always did.

## I started a second recording by accident

Nothing guards against it. `record` builds its directory and starts the tap
without ever asking whether a recording is already live, so a second
`ambient record` in a terminal — usually one started alongside the menu bar app
— gives you two taps, two directories and two sets of scratch wavs. The menu bar
cannot get you here on its own: its Start item does nothing unless the app is
Idle or Armed, and Stop is enabled only while it is Recording.

A bare `ambient stop` will not undo it. `live_session` collects every session
whose `audio/room.native.wav` was written to in the last ten seconds, sorts and
pops, so what comes back is the last directory name in sort order. That is the
newest when the two started in different minutes, which is the ordinary case —
but the name is a minute-resolution timestamp plus a `--name` slug, so inside
one minute the slug decides and a named session sorts after an unnamed one
whatever the clock said. Either way the stop reaches one of them, the other
carries on, and nothing on screen says so.

The way out is to name the directory:

```sh
ambient stop ~/Documents/Ambient/2026-08-29T1223-standup
```

`stop_recording` checks only that the path is a directory before writing the
`STOP` file the capture loop notices on its next 200 ms tick, so an explicit
path reaches the session the search will never return. List the sessions folder
— `ambient config` prints where it is — and the two in flight are the two
newest names. Why the mechanism can only answer "the newest" is in
[sessions and the edit layer](../developing/sessions.md).

Two unnamed recordings begun inside the same minute used to be the sharper
version of this, and no longer are. A session directory is now *claimed* rather
than assumed: `create_dir` — not `create_dir_all` — fails with `AlreadyExists`
on a name something else already holds, so the second recording retries with a
zero-padded suffix and gets `2026-08-29T1223-02` of its own. Two captures, two
directories, two `STOP` files. The padding is not decoration: every listing
here sorts names as bytes, and an unpadded `-10` would sort before `-2`.

What the suffix does not fix is the paragraph above it. Both sessions are live
and both are growing, so a bare `ambient stop` still reaches whichever of them
sorts last and leaves the other running; naming the directory is still the way
to reach a specific one.

## A session directory with only audio/ in it

A recording killed part-way through leaves a directory that looks like a session
and is not one: `audio/room.native.wav` and `audio/call.native.wav` at the
devices' own rates, a `status` file whose last line still begins `recording`,
and nothing else.

The order of writes is what makes it recognisable. Both native wavs are created
before the capture loop starts; `session.json` is written at the very end, after
the resample, after the recogniser and after `raw.jsonl`. Nothing that dies
during capture gets near the second, so natives with no `session.json` is a
recording that was killed rather than one that finished.

Those scratch files are also how a live recording is found, which is why
`live_session` insists the file was written to within the last ten seconds —
without that check the corpse answers to `ambient stop`, pointing it at a
directory nothing is writing to. The menu's level line is no longer exposed to
any of this. It used to lose the scratch wavs at the start of transcription and
fall back to whichever directory in the sessions folder sorted last — which was
`app.log`, since a loose file sorts after every `2…` session id. The line now
reads a meter the capture worker shares in memory, so it never consults the
folder at all, and `app.log` has moved out of the sessions folder to
`~/Library/Logs/Ambient/app.log` besides.

Nothing will ever reclaim it. Retention skips any session with no
`transcript.md`, on the grounds that the audio is the only copy of what was
said, and this directory will never have one, so its wavs stay for as long as it
does — see [what is kept](what-is-kept.md). Delete it by hand. There is no
`raw.jsonl` behind it and no edit layer to lose, so `rm -r` on the directory
costs nothing but the audio nobody transcribed.

## The disk filled

The failure surfaces late, not at the write that fills the disk.
`WavWriter::create` wraps the file in a `BufWriter`, so `write_sample` keeps
returning `Ok` into an 8 KB buffer and the `ENOSPC` comes back from whichever
sample happens to trigger a flush — up to a buffer's worth of audio is gone
with no error attached to it. That error then returns out of `record` into
`main`, which returns a `Result`, so the failure prints to stderr and the
process exits non-zero. Nothing is finalised: both `finalize()` calls sit after
the capture loop and are never reached.

The two writers are dropped instead, and `hound`'s `Drop` calls `update_header`
and throws the result away — a drop must not panic, so its own comment says the
failure is ignored silently. The header patch itself only overwrites four bytes
that already exist, which needs no new space; what fails is the seek in front
of it, because `BufWriter`'s `Seek` flushes the still-buffered samples first.
So `update_header` returns early, the data-chunk length written as zero at
creation stays zero, and the wav declares no samples however many bytes are in
it. That has not been reproduced against a genuinely full disk here; it is read
off `hound`'s `write.rs` and `src/session.rs`.

Launched as a bundle you will see none of that, because stderr has nowhere to
go. The menu bar writes its own log to `~/Library/Logs/Ambient/app.log` — a
fixed path, unaffected by `sessions_dir` and `AMBIENT_HOME`, and deliberately
outside the sessions folder, which should hold only sessions. A recording that
fails appends one line there: `FAILED — <context>: <error>`, where the context
names what was being attempted — `recording` when the capture fails,
`transcribing <dir>` when the transcript does — and the error is formatted with
`{e:#}`, so the whole cause chain is in that line and not just its outermost
message. It has to be: stderr, where you would otherwise read the chain, is
exactly what a bundle launch cannot show you. That line is no longer the only
sight of a failure — the window shows the failure in its banner, and the menu
bar sits in a `Failed` state until it is dismissed — but it is still the first
file to open when a recording ends with no transcript.

None of this weakens the append-only guarantee. `raw.jsonl` is created after the
capture loop, after both `finalize()` calls and after the resample, and
`edits.jsonl` a few lines after that, so a write failure during capture returns
before either file exists. There is no half-written record layer to fold, and
what is left behind is the corpse described above. See
[ADR-0008](../adr/0008-append-only-raw.md).

Source: https://jasonm4130-labs.github.io/ambient/using/troubleshooting/index.mdx
