[BUG] macOS: Claude CLI registers as a foreground Ghostty instance, leaving duplicate Dock icons
What's wrong?
On macOS, running concurrent terminal-based agents leaves many duplicate Ghostty icons in the Dock, without corresponding visible terminal windows. During investigation, Launch Services identified a running Claude Code executable as a second foreground Ghostty application, even though only one actual Ghostty app process was running.
This report separates the observed Claude registration from a small independent AppKit reproduction. The latter demonstrates that a GUI parent's application identity can be assigned to its CLI child; it does not establish which Claude operation triggers that behavior.
Environment
- macOS 27.2, build
26B5091g; Apple M5 Max. - Ghostty 1.3.1; shell: zsh.
- Claude Code:
2.1.288 (Claude Code), verified by running the observed stable executable with--versionafter capture. The version at the initial incident was not separately recorded. - The installed version matches the latest GitHub release checked during this investigation: v2.1.288, published October 2, 2026.
- Claude executable was running from
~/.local/share/tcc-stable/<hash>/claude. - This is a custom stable executable/launch path and is a potential confounder. The stock CLI launch path has not been independently reproduced.
- Model/API platform: not established for the captured process; regression/last working version: unknown.
What should happen?
Claude's terminal process and its background helpers should run without creating an additional Dock-visible instance of the hosting terminal. The real Ghostty window, Dock icon, and running agent sessions should remain usable.
Observed process evidence
There was one actual Ghostty executable, PID 52469. A separate live Claude executable appeared in lsappinfo list with these fields (personal paths and hash redacted):
name: Ghostty
bundleID="com.mitchellh.ghostty"
bundle path="/Applications/Ghostty.app"
executable path="~/.local/share/tcc-stable/<hash>/claude"
type="Foreground"
originalExecutablePath="/Applications/Ghostty.app/Contents/MacOS/ghostty"
originalPid=52469
This is a field extract, not an unmodified full log. One impostor registration was directly confirmed. Although the user observed many duplicate icons while concurrent agents were active, the evidence does not map every icon to a particular Claude process, nor establish that Claude was the source of every icon. The exact CLI operation immediately preceding registration is unknown.
Restarting the Dock with killall Dock preserved the real Ghostty process and all 15 terminal/agent processes checked before and after. Visual verification of the resulting Dock timed out, so this test confirms session preservation, not that every extra icon disappeared. It does not demonstrate prevention of recurrence.
Reproduction status
The original workflow is: launch terminal agents in Ghostty, run concurrent agent work, and observe duplicate terminal icons accumulating. That is the reported scenario, not yet a deterministic stock-Claude reproduction. No assertion is made that starting any fresh Claude process is sufficient.
To isolate the macOS mechanism, an independent Swift GUI app spawned a separate unbundled CLI executable that initialized NSApplication.shared and called finishLaunching(). Launch Services registered that child as the GUI parent's foreground application. The child's own Bundle.main.bundleIdentifier was nil.
Two sequential cases were tested from the same GUI parent:
| Child environment | Child AppKit policy | Launch Services registration |
| --- | --- | --- |
| __CFBundleIdentifier set to GUI parent's ID | 0 (regular) | Parent's name, bundle ID and bundle path; child executable; Foreground |
| __CFBundleIdentifier removed | 0 (regular) | Same parent identity and Foreground registration |
Both records also identified the GUI host executable and PID as originalExecutablePath and originalPid. Thus, removing __CFBundleIdentifier alone did not prevent the behavior in this GUI-parent experiment. Separate probes started outside that GUI-parent context registered as background-only, so that launch context matters.
Independent AppKit reproduction
The following is the tested source with the temporary directory, app name, and bundle identifier replaced by generic equivalents. It uses no Claude code or credentials. Place both sources in /tmp/dock-parent-repro; compile the child there as probe. Package the host as a normal .app named Dock Parent Repro.app, with executable dock-test and bundle identifier example.dock-parent-repro, then launch that bundle with open. The host writes host-results.json in the temporary directory. Each child remains alive for three seconds so the host can capture its registration.
probe.swift:
import AppKit
import Foundation
let app = NSApplication.shared
if CommandLine.arguments.contains("accessory") {
app.setActivationPolicy(.accessory)
}
app.finishLaunching()
let info: [String: Any] = [
"pid": ProcessInfo.processInfo.processIdentifier,
"bundle_id": Bundle.main.bundleIdentifier ?? "none",
"activation_policy": app.activationPolicy().rawValue,
"inherited_bundle_id": ProcessInfo.processInfo.environment["__CFBundleIdentifier"] ?? "none"
]
print(String(data: try! JSONSerialization.data(withJSONObject: info, options: [.sortedKeys]), encoding: .utf8)!)
fflush(stdout)
RunLoop.current.run(until: Date().addingTimeInterval(3))
app.setActivationPolicy(.prohibited)
host.swift:
import AppKit
import Foundation
let app = NSApplication.shared
app.finishLaunching()
let root = URL(fileURLWithPath: "/tmp/dock-parent-repro")
var results: [[String: Any]] = []
for clean in [false, true] {
let child = Process()
child.executableURL = root.appendingPathComponent("probe")
var env = ProcessInfo.processInfo.environment
if clean { env.removeValue(forKey: "__CFBundleIdentifier") }
else { env["__CFBundleIdentifier"] = "example.dock-parent-repro" }
child.environment = env
let output = Pipe()
child.standardOutput = output
child.standardError = FileHandle.nullDevice
do {
try child.run()
Thread.sleep(forTimeInterval: 0.4)
let listing = Process()
listing.executableURL = URL(fileURLWithPath: "/usr/bin/lsappinfo")
listing.arguments = ["list"]
let listingOutput = Pipe()
listing.standardOutput = listingOutput
try listing.run()
let raw = String(data: listingOutput.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
listing.waitUntilExit()
child.waitUntilExit()
let childOutput = String(data: output.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let lines = raw.components(separatedBy: "\n")
let match = lines.firstIndex { $0.contains("pid = \(child.processIdentifier) ") }
let registration = match.map { i in lines[max(0,i-4)...min(lines.count-1,i+3)].joined(separator: "\n") } ?? "absent"
results.append(["clear_bundle_env":clean,"child_output":childOutput,"registration":registration])
} catch {
results.append(["error":String(describing:error)])
}
}
let data = try! JSONSerialization.data(withJSONObject: results, options: [.prettyPrinted,.sortedKeys])
try! data.write(to: root.appendingPathComponent("host-results.json"))
app.setActivationPolicy(.prohibited)
After saving those two files in /tmp/dock-parent-repro, build and run the fixture with:
mkdir -p '/tmp/dock-parent-repro/Dock Parent Repro.app/Contents/MacOS'
swiftc /tmp/dock-parent-repro/probe.swift -o /tmp/dock-parent-repro/probe
swiftc /tmp/dock-parent-repro/host.swift -o '/tmp/dock-parent-repro/Dock Parent Repro.app/Contents/MacOS/dock-test'
cat > '/tmp/dock-parent-repro/Dock Parent Repro.app/Contents/Info.plist' <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleIdentifier</key><string>example.dock-parent-repro</string>
<key>CFBundleExecutable</key><string>dock-test</string>
<key>CFBundleName</key><string>Dock Parent Repro</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleVersion</key><string>1</string>
</dict></plist>
PLIST
open -gj '/tmp/dock-parent-repro/Dock Parent Repro.app'
Wait for the two three-second child runs to finish, then read /tmp/dock-parent-repro/host-results.json. The fixture creates temporary test app registrations; each child and the host exits automatically. It does not interact with existing terminal sessions.
The observed registration structure in both GUI-parent cases was:
"Dock Parent Repro"
bundleID="example.dock-parent-repro"
bundle path="/private/tmp/dock-parent-repro/Dock Parent Repro.app"
executable path="/tmp/dock-parent-repro/probe"
pid = <child-pid> ... type="Foreground"
originalExecutablePath="/private/tmp/dock-parent-repro/Dock Parent Repro.app/Contents/MacOS/dock-test"
originalPid=<gui-host-pid>
Additional local mitigation test
On the disposable GUI-parent fixture only, I tested changing the live child's classification with /usr/bin/lsappinfo setinfo "#<child-pid>" -uielement. The installed tool documents #PID as the PID selector. Both the short option and ApplicationType=UIElement returned exit status 0; verbose output showed the intended child ASN and requested ApplicationType=UIElement.
However, a subsequent registration read after a 0.75-second delay still reported type="Foreground". The child remained running and later exited normally. Therefore, this was not a verified mitigation on this system; a successful command exit alone did not prove a classification change. No such change was applied to a real Claude or Ghostty process.
Requested investigation
Could maintainers investigate whether a Claude native component initializes AppKit or promotes the CLI process to a regular foreground application, and whether that component can use an accessory/background activation policy while preserving its intended functionality? This is a proposed direction, not a fix verified in Claude. A supported setting or diagnostic path to identify the responsible component would also help.
The custom stable launch path and the macOS build should be considered when triaging. The independent probe narrows the operating-system mechanism but does not exclude custom-launch effects or prove a Claude-specific root cause.
Related reports
- #91591 describes Dock icons for Cowork/desktop-spawned Claude sessions despite
LSBackgroundOnly. This report instead concerns a terminal CLI executable registered under Ghostty's identity. - stablyai/orca#21768 reports an analogous terminal-parent/agent-child identity pattern. It is corroborating context, not evidence that the same component is responsible here.
Existing issues were searched. This report concerns one symptom. The current installed Claude version matches the latest published release checked above; the initial event's exact version was not independently captured.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗