While looking for something else in a slow Mac's logs, one system service stood out: 39,000 error lines in two hours, sixteen orphaned copies of itself, and every one of them pointing at the terminal. The terminal was not doing anything unusual. It was doing something normal, at a rate nobody designed for, and macOS 27 had started saying no.
The short versionGhostty exposes each terminal as an App Intents entity, and on macOS 27 Apple's App Intents runtime, running inside Ghostty's process, re-donates that entity to Spotlight whenever the tab title changes. macOS 27's Biome store refuses the donation, as it does for every third-party app on this machine. AI coding agents animate a spinner in the tab title several times a second, so one busy tab becomes a stream of rejected donations. Fix the title churn at the agent and the loop stops. Restarting Ghostty does not help, and neither does the current Ghostty tip build: Ghostty has no Spotlight code to fix.
A noisy service nobody asked for
The starting point was an ordinary post-upgrade audit on a 16 GB MacBook Air that had just moved to macOS 27.0. Memory pressure turned out to be the real slowness, and it had nothing to do with the operating system. But tallying error-level log senders for the previous two hours produced a leaderboard with a surprising first place:
$ /usr/bin/log show --last 2h --style compact \
| awk '$3=="E"||$3=="F"{print $4}' | sort | uniq -c | sort -rn | head -4
39252 SetStoreUpdateService
11971 cfprefsd
9661 WindowServer
2762 mobileassetd
SetStoreUpdateService is an XPC service inside Apple's private CascadeSets framework, part of the Biome subsystem that stores the on-device behavioral data behind Siri suggestions and Spotlight. Sixteen copies of it were alive at once, each a few megabytes, each parentless. The messages were all variations of the same refusal:
[BiomeCascade] Refusing connection from ghostty(60309),
process not properly entitled
[BiomeGeneral] Warning: Not trusting process ghostty(60309)
with identifier (null) (validation category 6, internal 0)
(libxpc.dylib) Peer connection was rejected by the listener
Ghostty had been launched after the macOS upgrade, not before it, so this was not a stale process holding an old entitlement. A fresh Ghostty on macOS 27 gets the same answer.
Note the path to log. In zsh, log is a shell builtin, so a bare log show fails with too many arguments and looks like a syntax mistake. The full path avoids the collision.
What a Spotlight donation is
Since macOS 15, an app can expose its own objects to Spotlight and Siri through App Intents. The app declares an entity type, for instance a document, a project, or in Ghostty's case a terminal surface, and conforms it to IndexedEntity. When the app creates or changes one of those objects, it hands a searchable record of it to the system. Apple calls that hand-off a donation: the app donates the entity to the index, and Spotlight can later show it as a result or Shortcuts can act on it.
Ghostty added Shortcuts support in 1.2. Each terminal is a TerminalEntity with a title, a working directory, and an identifier, which is what lets a Shortcut find and target a specific tab. Reading the source shows that this is a plain AppEntity: Ghostty never conforms it to IndexedEntity, never imports CoreSpotlight, and never calls a donation API. Yet the log shows CoreSpotlight donations being performed from Ghostty's own threads, filed under set/App.Intents.IndexedEntity. The donating code is Apple's App Intents runtime, linked into every app that declares entities. On macOS 27 it indexes those entities for Spotlight on its own, and every time a terminal's title changes, the entity is stale, so it donates again.
Under the hood, the donation goes through CoreSpotlight, which forwards it to Biome's set store. That store is where SetStoreUpdateService lives. On macOS 27 the store checks the donating process against an entitlement it now expects, and refuses the connection when it is missing. Over six hours on this Mac, every refused process was third-party: Ghostty, Chrome for Testing, CleanMyMac, iStat Menus, Setapp, Jiggler. Every accepted donation came from an Apple process: System Settings, Siri, Finder, the help daemon. Whatever the entitlement is, no third-party app has it, so this is a macOS 27 policy rather than a Ghostty omission. CoreSpotlight reports the failure back to the app as a generic error:
[com.apple.corespotlight:index] Performing donation
[com.apple.Biome:BiomeCascade] Requesting new set donation
[com.apple.Biome:BiomeCascade] Service responded with None
[com.apple.corespotlight:index] Completed donation
result: Error Domain=CSIndexErrorDomain Code=-1000
"Failed to request donation"
None of this is a Ghostty bug. It is Apple's framework doing inside Ghostty exactly what it does inside every app with entities, meeting an Apple service that no longer admits it. Ghostty is simply the app whose entities change most often.
Why the loop exists
A single refused donation would be a one-line curiosity. The loop comes from the combination of three facts, none of which is wrong on its own.
+--------------------+ title changes +----------------------+
| Ghostty terminal |---------------------->| CoreSpotlight donate |
| (App Intents | | IndexedEntity |
| TerminalEntity) |<----------------------| |
+--------------------+ CSIndexError -1000 +----------+-----------+
^ |
| next title change v
| +----------------------+
| | Biome set store |
| | SetStoreUpdateService|
| | "not properly |
| | entitled" -> reject |
+---------------------------------------------------------+
- The App Intents runtime inside Ghostty donates on every title change, because that is how it keeps an indexed entity current.
- macOS 27 rejects the donation, and the rejection is a plain error with no back-off hint, so nothing tells the app to stop trying.
- Each rejected connection spawns or wakes an instance of
SetStoreUpdateServiceto say no, and those instances outlive the request.
The rate of the loop is therefore not a property of Ghostty or of macOS. It is exactly the rate at which tab titles change. In an idle shell that is a few times a minute: a new prompt, a directory change, a command starting. Something else was changing titles far faster than that.
Why the agents are the trigger
Sampling Ghostty's window names three seconds apart showed the culprit directly. The first sample read ⣀ Localization QA findings - amp - ~/work/.... The second read ⠿ Localization QA findings - amp - ~/work/.... The braille glyph at the front is a spinner. Amp, Claude Code, and Codex all write their working state into the terminal title, and while they are thinking they animate it, several frames a second, in every tab where one is running.
Counting donations per Ghostty thread made the shape unmistakable. Each thread corresponds to one terminal surface, and the busy ones were all running an agent:
$ /usr/bin/log show --last 60s --style compact \
--predicate 'process == "ghostty" AND eventMessage CONTAINS "Performing donation"' \
| grep -oE 'ghostty\[[0-9]+:[0-9a-f]+\]' | sort | uniq -c | sort -rn
31 ghostty[60309:29ebc6]
30 ghostty[60309:29ebba]
28 ghostty[60309:29db74]
27 ghostty[60309:29ebb9]
25 ghostty[60309:29ed8a]
24 ghostty[60309:29ebbd]
20 ghostty[60309:29db75]
18 ghostty[60309:29ebbb]
Eight surfaces at twenty to thirty donations a minute each, plus a tail of quieter tabs, gave a total of 209 donations in one sixty-second window. The Biome refusals matched one for one. And the count climbed through the day as more agent sessions were opened: 1,700 refusals in the 16:00 hour, 3,500 at 17:00, 5,000 at 18:00, 7,000 at 19:00.
This is the part worth generalising. A terminal title used to be a slow-moving label, updated by a shell hook at the prompt. Coding agents turned it into a progress indicator. Any terminal feature that reacts to title changes, whether it is a Spotlight donation, a window-manager rule, a tmux hook, or an accessibility announcement, now runs at animation speed in every tab with an agent in it. The spinner is cheap for the agent to draw. It is not necessarily cheap for whatever is listening.
Measuring it
Three one-line checks are enough to confirm the loop on any machine. Run them from a terminal and give the last one a minute.
# Is Biome refusing your terminal?
/usr/bin/log show --last 10m --style compact \
--predicate 'eventMessage CONTAINS "Refusing connection from"' \
| sed -E 's/.*Refusing connection from ([^(]+)\(.*/\1/' | sort | uniq -c
# How many orphaned set-store services are alive?
pgrep -c SetStoreUpdateService
# Donations per minute from the terminal process
/usr/bin/log show --last 60s --style compact \
--predicate 'process == "ghostty" AND eventMessage CONTAINS "Performing donation"' \
| grep -c Performing
The predicate names the process, so substitute your terminal's process name if it is not Ghostty. Any app that conforms an entity to IndexedEntity and updates it often would show the same pattern. The donation lines are at the default log level, which is why they do not show up in an error-only tally; only the service's refusals do.
What did not work
The obvious levers were tried first, and each one taught something about where the loop actually lives.
Restarting Ghostty
Not tried, because the evidence already ruled it out. The running Ghostty had been launched two days and three hours earlier; macOS 27 had been installed two days and thirteen hours earlier. The process was born on macOS 27 and had been refused from its first donation. A restart resets the orphaned service processes and Ghostty's memory footprint, both of which are minor, and then the first spinner frame starts the loop again.
The Siri "Learn from this App" switch
System Settings has a per-app toggle that stops Siri learning from an app. It writes the bundle identifier into SiriCanLearnFromAppBlacklist in com.apple.suggestions. Adding Ghostty there and measuring again gave 190 donations a minute against a 209 baseline, which is noise. The switch is consulted on the Biome side, after the connection has already been attempted and refused. The app never learns about it, so it keeps donating. The setting was reverted.
Killing the orphans
killall SetStoreUpdateService clears the sixteen stale processes instantly and harmlessly. Within a minute the next refusal spawns a fresh one. It is tidying, not fixing.
Pinning the window title in Ghostty
This one worked, and it is the diagnostic that proved the mechanism. Ghostty's title option forces one title on every window and is reloadable at runtime with the default super+shift+, keybind. Setting title = "Ghostty" and reloading dropped donations from 209 a minute to 9. No restart, no new process. But every tab now read "Ghostty", which throws away the one thing a terminal title is for. It is a good proof and a bad fix.
Stopping the source instead of the sink
Once the pinned title had shown that the loop is driven purely by title churn, the right place to intervene was the programs doing the churning. The shell's own title updates are rare and useful. The agents' spinner frames are frequent and decorative. Each agent turned out to have a different answer.
| Agent | Title switch | Effect |
|---|---|---|
| Codex CLI | tui.terminal_title = ["project"] in config.toml | Keeps the project name, drops the spinner. Documented; default is ["spinner", "project"]. |
| Claude Code | CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 in the environment | Leaves the title alone entirely. Present in the binary, not in the public docs. New sessions only. |
| Amp | none found | No setting, flag, or variable in the manual, docs, --help, or the binary. Its tabs keep spinning. |
For Codex, the setting sits under the existing [tui] section and applies to the next session:
[tui]
animations = false
terminal_title = ["project"]
For Claude Code, the variable goes wherever the shell picks it up. Sessions already running keep their spinner until they are restarted; claude --continue in the same tab brings each one back with the setting in effect:
# ~/.zshrc
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1
With Ghostty's title line removed again, tabs went back to their live names. The measured rate with the old agent sessions still running was 64 donations a minute, down from 209, and it falls further as those sessions are restarted. What remains is the amp tabs plus the shell's ordinary title changes, which was the floor all along.
Do not expect a Ghostty update to end it. Ghostty's tip build as of 18th September 2026 fixes the macOS 27 tab bar, but its App Intents changes since 1.3.1 only make a new TerminalEntity wait for its title and working directory before it is created; the entitlements file and Info.plist are unchanged, and there is still no Spotlight code to throttle. The refusal is Apple's to lift, either by admitting third-party donations again or by not auto-indexing entities that were never marked for indexing. Ghostty could soften it by updating the entity less eagerly, but until either side moves, the agents are the adjustable part.
What it actually costs
Honesty requires saying that this loop was not what made the Mac slow. The Biome-side processes together used about 0.1% CPU. The log churn from the loop came to roughly 13 MB an hour. Ghostty itself sat at 10 to 12% CPU, but most of that was rendering the agents' output, not the donations. The genuine slowness on that machine was 2.8 GB of swap on a 16 GB laptop and three low-memory kill events in a week, caused by a search tool walking into 60 GB of database dumps. That is a different article.
The loop matters for a different reason. It is a clean example of a class of problem that coding agents are going to keep producing: a background behavior that was harmless at human speed becomes a hot path at animation speed, and it happens inside a process the user did not know was involved. The terminal did not change. The operating system did not change in a way the terminal was told about. The workload changed the tempo, and the tempo exposed the seam.
The practical advice is short. If a system service you have never heard of tops the error log, find out which of your processes it is refusing before assuming the OS is at fault. Measure with the log, not with a feeling. And when an agent offers a switch to stop animating something outside its own window, use it.
Sources
- Apple, IndexedEntity, App Intents documentation
- Apple, Core Spotlight documentation
- Ghostty, 1.2.0 release notes: Apple Shortcuts integration
- Ghostty source, TerminalEntity.swift on main
- Ghostty, Meta: macOS Golden Gate (v27), issue 13001
- Ghostty, macOS 27 Golden Gate: tab bar is broken, discussion 14244
- OpenAI, Codex CLI configuration reference: tui.terminal_title
- Anthropic, Claude Code issue 17887: terminal title spinner animation
- Sourcegraph, Amp owner's manual
- Apple, WWDC26: Discover new capabilities in the App Intents framework
- Apple, macOS 27 release notes