[BUG] Claude Code app bundle lacks NSBluetoothAlwaysUsageDescription — child processes get hard-killed instead of prompted with Routines

Status Open
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jul 10, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When Claude Code (or a process it spawns, e.g. via the Bash tool) triggers a macOS Bluetooth permission check (kTCCServiceBluetoothAlways), the request is refused outright instead of showing the normal user permission dialog - especially in Claude Code Routines!

This happens because Claude Code's app bundle Info.plist does not declare an NSBluetoothAlwaysUsageDescription key. Without this key, macOS's TCC daemon cannot show a permission prompt at all — it silently denies the request. Confirmed via log show:

Refusing authorization request for service kTCCServiceBluetoothAlways and subject
Sub:{com.anthropic.claude-code}Resp:{TCCDProcess: identifier=com.anthropic.claude-code, ...}
without NSBluetoothAlwaysUsageDescription key
For comparison, other apps that do declare this key (e.g. Terminal.app) get a normal prompt (AUTHREQ_PROMPTING → user clicks Allow → Granting), and everything proceeds normally.

Impact: Any child process launched from Claude Code (e.g. a browser automated via Bash/Playwright) that happens to trigger a Bluetooth-related API call — even indirectly, e.g. via a website's device-fingerprinting/bot-detection script — gets hard-killed by macOS (SIGABRT) instead of just being denied Bluetooth access gracefully. The parent Claude Code session survives, but the spawned process crashes outright, which can silently break long-running automation tasks with no clear error message pointing to the real cause (it just looks like a random crash).

Expected behavior: Either (a) Claude Code's Info.plist should declare NSBluetoothAlwaysUsageDescription so macOS can prompt the user normally instead of hard-refusing, or (b) at minimum, child processes should be able to fail gracefully (Bluetooth API call returns "denied") rather than being terminated.

Workaround found: Running the same script via a user LaunchAgent (launchd) instead of through Claude Code avoids the issue entirely, because the "responsible process" TCC attributes the Bluetooth request to is then the script's own interpreter, not Claude Code.

What Should Happen?

Workaround found: Running the same script via a user LaunchAgent (launchd) instead of through Claude Code avoids the issue entirely, because the "responsible process" TCC attributes the Bluetooth request to is then the script's own interpreter, not Claude Code.

Error Messages/Logs

Steps to Reproduce

Workaround found: Running the same script via a user LaunchAgent (launchd) instead of through Claude Code avoids the issue entirely, because the "responsible process" TCC attributes the Bluetooth request to is then the script's own interpreter, not Claude Code.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.20186.0 (d7731e)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗

4 Comments

gruMoses · 1 month ago

Independent confirmation with additional evidence, from driving Bluetooth serial (SPP) lab hardware through Claude Code desktop's Bash tool on macOS.

Symptoms match exactly: any child process spawned from Claude Code that touches the Bluetooth stack (pyserial/IOBluetooth in our case) is hard-killed with SIGABRT (shell sees exit 134) — silently, with no TCC prompt and no entry in System Settings → Privacy & Security → Bluetooth to grant after the fact. Attribution goes to the embedded com.anthropic.claude-code bundle, which lacks NSBluetoothAlwaysUsageDescription, so tccd refuses without prompting.

Additional data points beyond the original report:

  • Granting Bluetooth to the visible "Claude" app entry (com.anthropic.claudefordesktop) does nothing for these child processes — the responsible-process attribution lands on the embedded com.anthropic.claude-code bundle, not the outer app. There is no user-visible path to authorize it.
  • Workaround that holds up in daily use: an ad-hoc-signed wrapper .app carrying NSBluetoothAlwaysUsageDescription hosts the Bluetooth-touching daemon; Claude Code talks to it over a local socket. Two gotchas for anyone reproducing:
  1. LaunchServices ran the wrapper's script executable under Rosetta, which breaks arm64-only native wheels (e.g. pydantic_core) — the wrapper must exec with arch -arm64.
  2. The LaunchServices environment lacks the Homebrew PATH; set it explicitly in the wrapper.
  • Effect: any Claude Code desktop user driving BT peripherals (hardware bring-up, BLE tooling, device test rigs) hits a silent hard-deny that presents as a crashing tool, not a permissions issue. Cost us several hours to diagnose the first time.

Fix request (same as OP): declare NSBluetoothAlwaysUsageDescription in the embedded bundle's Info.plist so tccd can show the normal prompt.

Environment: macOS (Apple Silicon, Darwin 25.x), Claude Code desktop app, processes spawned via the Bash tool.

bcherny collaborator · 14 days ago

Thanks for the detailed report and the log evidence. Confirmed / reproduced on 2.1.233 (macOS, regression since 2.1.143 for background sessions).

What I tried: started a background agent session with claude --bg on 2.1.233 and had it run a small CoreBluetooth program (creates a CBCentralManager). No Bluetooth permission prompt ever appeared, the authorization status stayed "not determined", and nothing showed up under System Settings > Privacy & Security > Bluetooth to grant after the fact. The same program in a normal terminal gets the standard prompt.

Root cause matches your analysis: background/desktop-launched Claude Code sessions run as their own macOS "responsible process" (so permission grants stick across upgrades), and that app bundle declares usage descriptions for microphone, Apple Events and local network but not Bluetooth, so macOS silently refuses instead of prompting. Fix is to declare NSBluetoothAlwaysUsageDescription in that bundle, same as was done for local network in 2.1.198.

I did not see the SIGABRT crash with CoreBluetooth in my test, only the silent denial. If you have a minimal script (e.g. the pyserial/IOBluetooth call) that crashes, please attach it so we can verify the crash goes away with the fix too.

We'll follow up here when the fix ships. Note the Claude desktop app bundles its own copy, so it may pick up the fix on a separate release cadence.

🤖 Generated with Claude Code

gbggit · 13 days ago

Follow-up from the reporter: minimal crashing script below, plus the crash
report. Short answer to your open question -- macOS terminates it. The crash
report records termination namespace "TCC", so this is enforcement, not Chrome
aborting itself.

What triggers it
----------------
navigator.bluetooth.requestDevice(), which starts a real scan and therefore
needs kTCCServiceBluetoothAlways. Worth noting: getAvailability() is NOT a
trigger -- it only reports whether an adapter exists and never reaches TCC (I
tested it, returns true cleanly, in both an interactive session and a scheduled
one). A CoreBluetooth probe that only checks state may therefore miss the crash.

Minimal reproduction
--------------------
# pip install playwright && playwright install chrome ; Bluetooth switched on
import pathlib, tempfile
from playwright.sync_api import sync_playwright

# file:// because Web Bluetooth needs a secure context (about:blank has an
# opaque origin); the button because requestDevice() needs user activation.
HTML = """<!doctype html><html><body>
<button id="go">scan</button>
<script>
window.__done = null;
document.getElementById('go').addEventListener('click', () => {
navigator.bluetooth.requestDevice({acceptAllDevices: true})
.then(d => { window.__done = 'RESULT:' + d.name; })
.catch(e => { window.__done = 'ERROR:' + e.name + ':' + e.message; });
});
</script></body></html>"""

path = pathlib.Path(tempfile.mkdtemp()) / "probe.html"
path.write_text(HTML)
with sync_playwright() as pw:
browser = pw.chromium.launch(channel="chrome", headless=False)
page = browser.new_page()
page.goto(path.as_uri())
print(f"chrome {browser.version}, clicking ...", flush=True)
try:
page.click("#go", timeout=10_000)
for _ in range(20):
done = page.evaluate("() => window.__done")
if done:
print("OUTCOME:", done, flush=True); break
page.wait_for_timeout(1_000)
else:
print("OUTCOME: still scanning (chooser open)", flush=True)
except Exception as e:
print("BROWSER DIED:", type(e).__name__, e, flush=True)
finally:
browser.close()

From a normal Terminal: prompt, then a normal OUTCOME line.
From a Claude Code Bash call, Chrome dies during the click:

BROWSER DIED: TargetClosedError: Page.click: Target page, context or browser
has been closed

Reproduced twice, 23:01:37 and 23:02:48 local time.

tccd log, three consecutive lines 6 ms apart
---------------------------------------------
23:01:37.819 AUTHREQ_PROMPTING: msgID=43368.22,
service=kTCCServiceBluetoothAlways,
subject=Sub:{com.anthropic.claude-code}Resp:{TCCDProcess:
identifier=com.anthropic.claude-code, pid=42216,
responsible_path=.../Claude/claude-code/2.1.229/claude.app/Contents/MacOS/claude}

23:01:37.824 Refusing authorization request for service
kTCCServiceBluetoothAlways and subject Sub:{com.anthropic.claude-code}...
without NSBluetoothAlwaysUsageDescription key

23:01:37.825 Google Chrome[43368]: This app has crashed because it attempted
to access privacy-sensitive data without a usage description.

Crash report (this is the SIGABRT you asked about)
----------------------------------------------------
~/Library/Logs/DiagnosticReports/Google Chrome-2026-08-17-230249.ips

"exception": {"type": "EXC_CRASH", "signal": "SIGABRT",
"codes": "0x0000000000000000, 0x0000000000000000"}
"termination": {"namespace": "TCC", "code": 0, "flags": 518,
"details": ["This app has crashed because it attempted to access
privacy-sensitive data without a usage description. The app's Info.plist
must contain an NSBluetoothAlwaysUsageDescription key with a string value
explaining to the user how the app uses this data."]}

namespace "TCC" means the kill comes from the TCC enforcement path against the
responsible process, so declaring the key should resolve the crash and the
silent denial in one go. I can attach the full .ips if useful.

Two things that may widen your repro scope
-------------------------------------------

  1. Not limited to background sessions. Both runs above were plain interactive

Bash tool calls in the Claude Desktop app, not claude --bg. I also ran the
getAvailability() variant as a scheduled task and it behaved identically to
the interactive one, so the session type does not seem to be the variable --
the API call is.

  1. The Desktop-bundled copy still lacks the key and is what TCC attributes to.

My CLI is on 2.1.233, but the responsible process above is 2.1.229 from
~/Library/Application Support/Claude/claude-code/. One log line showed the
accessing process as ~/.local/share/claude/versions/2.1.233 while the
responsible one was the 2.1.229 bundle -- a CLI-only fix will not reach
Desktop users.

$ /usr/libexec/PlistBuddy -c 'Print :NSBluetoothAlwaysUsageDescription' \
~/Library/Application\ Support/Claude/claude-code/2.1.229/claude.app/Contents/Info.plist
Print: Entry, ":NSBluetoothAlwaysUsageDescription", Does Not Exist
# NSAppleEvents / NSLocalNetwork / NSMicrophone usage descriptions: all present

Environment: macOS 15.7.9 (24G830), iMac19,x (Intel Core i5-8600),
Chrome 151.0.7922.138, Claude Desktop with embedded claude-code 2.1.229,
CLI 2.1.233.

gbggit · 3 days ago

Follow-up from the reporter, answering the open question above: here is a minimal
program that reproduces the hard kill. No pip install, no browser — just Swift,
which ships with the Xcode Command Line Tools.

Correction to my previous comment. I assumed a CoreBluetooth probe that only
checks state would not reach TCC. That is wrong. Merely constructing a
CBCentralManager is enough: the manager issues a state query that hits
kTCCServiceBluetoothAlways, and the process is terminated. So the crash is much
easier to reproduce than the navigator.bluetooth.requestDevice() path I posted
earlier.

Still present in 2.1.246 (embedded bundle, signature timestamp 2026-08-25).
Usage descriptions in that bundle: NSAppleEventsUsageDescription,
NSLocalNetworkUsageDescription, NSMicrophoneUsageDescription.
NSBluetoothAlwaysUsageDescription is still absent, and the entitlements list
com.apple.security.device.audio-input but no com.apple.security.device.bluetooth.

Minimal reproduction

bt_test.swift:

import CoreBluetooth
import Foundation

final class Probe: NSObject, CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ c: CBCentralManager) {
        let names = [0:"unknown",1:"resetting",2:"unsupported",3:"unauthorized",4:"poweredOff",5:"poweredOn"]
        print("RESULT state=\(c.state.rawValue) (\(names[c.state.rawValue] ?? "?"))")
        exit(0)
    }
}
let p = Probe()
let m = CBCentralManager(delegate: p, queue: nil)
RunLoop.main.run(until: Date().addingTimeInterval(8))
print("RESULT timeout state=\(m.state.rawValue)")
exit(7)

Run it from a Bash tool call inside a Claude Code session on macOS, Bluetooth
switched on. It never prints anything:

  • compiled — swiftc -O bt_test.swift -o bt_test && ./bt_test

Abort trap: 6, shell exit 134. That is the SIGABRT from the original report.

  • interpreted — swift bt_test.swift

→ the kill lands on swift-frontend, shell exit 137, and the stack trace names
the killer explicitly:

6  TCC   0x00007ff81a61d72e __TCC_CRASHING_DUE_TO_PRIVACY_VIOLATION__ + 142
7  TCC   0x00007ff81a61df4d __TCCAccessRequest_block_invoke.162 + 0
8  TCC   0x00007ff81a61b478 __tccd_send_message_block_invoke + 592

So it is macOS enforcement, not the program aborting itself.

Log at the moment of the crash

2026-08-27 10:01:55.516 tccd  AUTHREQ_CTX: service=kTCCServiceBluetoothAlways, preflight=no, query=1
2026-08-27 10:01:55.586 tccd  AUTHREQ_PROMPTING: service=kTCCServiceBluetoothAlways,
                              subject=Sub:{com.anthropic.claude-code}
                              Resp:{TCCDProcess: identifier=com.anthropic.claude-code, ...}
2026-08-27 10:01:55.589 tccd  Refusing authorization request for service
                              kTCCServiceBluetoothAlways and subject
                              Sub:{com.anthropic.claude-code} ...
2026-08-27 10:01:55.590 swift-frontend  This app has crashed because it attempted to access
                              privacy-sensitive data without a usage description. The app's
                              Info.plist must contain an NSBluetoothAlwaysUsageDescription key
                              with a string value explaining to the user how the app uses this data.

Environment

  • macOS 15.7.9 (24G830), Intel Core i5-8600, x86_64
  • Claude Code 2.1.246 (embedded bundle, run from Claude Desktop), CLI 2.1.241