Shows the macOS menu bar battery icon when on battery power, hides it when plugged into AC. Uses IOKit power source notifications (event-driven, no polling).
How it works
macOS stores the battery menu bar visibility in a ByHost preference:
defaults -currentHost write com.apple.controlcenter Battery -int 18 # show
defaults -currentHost write com.apple.controlcenter Battery -int 24 # hide
This tool watches for power source changes and runs the appropriate command.
Build
swiftc battery-icon-toggle.swift -o battery-icon-toggle
Install
# Copy the binary sudo cp battery-icon-toggle /usr/local/bin/ # Install the launchd agent cp com.dceddia.battery-icon-toggle.plist ~/Library/LaunchAgents/ # Load it launchctl load ~/Library/LaunchAgents/com.dceddia.battery-icon-toggle.plist
Uninstall
launchctl unload ~/Library/LaunchAgents/com.dceddia.battery-icon-toggle.plist rm ~/Library/LaunchAgents/com.dceddia.battery-icon-toggle.plist rm /usr/local/bin/battery-icon-toggle
Try It
Run it directly to test:
It sets the icon based on current power state immediately, then watches for changes. Plug/unplug to verify. Ctrl-C to stop.
How This Came To Be
I wanted to free up some space in my menu bar. Surveying the options, I more or less needed everything there, but... why is the battery icon there taking up valuable space when my laptop is plugged in 99.9% of the time?
You can hide this icon in System Settings. Control Center > Battery > Show In Menu Bar. Turn it off, done.
That toggle is permanent though. It doesn't care whether you're plugged in or not, which means, for me anyway, that the once in a blue moon that I unplug my laptop, I will absolutely forget to turn the battery icon back on, and run myself out of battery.
Seems logical for that icon to only appear while plugged in, right? Like that might be a standard feature I could toggle somewhere? Well, if that exists, I never found it.
So I made this little utility instead.
How I Made This
Claude Code helped a lot, but not in the way you might think. This turned into a reverse engineering journey involving a debugger.
My first prompt was:
I want to hide the macOS Battery icon from the menu bar when not plugged in. It's in system preferences as Control Center > Battery > Show in menu bar. Seems maybe com.apple.controlcenter.plist might store that.
I figured it could help me find the right preference setting and we'd be on our way. But no.
There was a setting called NSStatusItem Visible Battery but it was a red herring. It did not control the icon.
So then I thought, ok fine, nevermind, it's reverse engineering time.
ok what if we use a debugger on system settings and watch what happens when i clickl that toggle Acccessibiliyt Inspector says this about the button
Claude gave me some breakpoints to set.
none of those breakpoints got hit. the thing is an NSSwitch, can i break on that
So then it gave me some more.
breakpoint set -n "-[NSControl sendAction:to:]" didn't work (didn't trigger). what about NSSwitch itself
Fuck it, breakpoint on every possible NSSwitch method.
can I just set it on every method image lookup returns
And yet.
lol even with over 69 breakpoints none of them get hit when i toggle it
And then it gave me some more breakpoint ideas, but still no luck.
breakpoint set -r "SwiftUI.*Toggle" -> 51 symbols no stop
breakpoint set -n _CFPreferencesSetDomainValue breakpoint set -n CFPreferencesSetValue breakpoint set -n -[NSUserDefaults setObject:forKey:] breakpoint set -n -[NSUserDefaults setBool:forKey:]
also didn't stop
breakpoint set -n CFNotificationCenterPostNotification -> nope breakpoint set -n -[NSDistributedNotificationCenter postNotificationName:object:userInfo:deliverImmediately:] -> nope
It was at this point Claude started to get concerned.
This is wild. It's probably doing everything over XPC — sending a message to another process (like controlcenterui or cfprefsd) that does the actual preference write and notification.
It had me set breakpoints on xpc_connection_send_message and xpc_connection_send_message_with_reply and toggle the battery switch.
This actually worked! Breakpoint achieved. I inspected the arguments and told Claude what I found.
(lldb) po $x0
<OS_xpc_connection: connection[0x11cf36870] (from wire): { refcnt = 2, xrefcnt = 1, name = com.apple.xpc.anonymous.0x11cf36870, type = anonymous, state = checked in, cancelation = (0, <none>), mach = false,
privileged = false, bssend = 0x1e707, recv = 0x1e403, send = 0x17003, pid = 6006, euid = 501, egid = 20, asid = 100003, channel: <OS_dispatch_mach: com.apple.xpc.anonymous.0x11cf36870[0x600001f1e520] = { xref
= 1, ref = 2, target = com.apple.NSXPCConnection.user.endpoint.NSRemoteView._EXExtensionRootViewController[0x6000003a3400], receive = 0x1e403, send = 0x17003, send-possible = 0x0, checkin = 0x1e707, send
state = 0000000000000000, disconnected = 0, canceled = 0 }> } <connection: 0x11cf36870> { name = (anonymous), listener = false, pid = 6006, euid = 501, egid = 20, asid = 100003 }>
(lldb) po $x1
<OS_xpc_dictionary: dictionary[0x60000097d2c0]: { refcnt = 1, xrefcnt = 1, subtype = 0, count = 6 } <dictionary: 0x60000097d2c0> { count = 6, transaction: 0, voucher = 0x0, contents =
"f" => <uint64: 0xba03d643bd007aa7>: 33
"ool" => <array: 0x6000023d47e0> { count = 1, capacity = 10, contents =
0: <mach send right: 0x600002d90140> { name = 128107, right = send, urefs = 0 }
}
"root" => <data: 0x600003785e40>: { length = 189 bytes, contents = 0x62706c6973743137a0bc000000000000007f112f5f5f6265... }
"proxynum" => <uint64: 0xba03d643bd007ba7>: 1
"replysig" => <string: 0x6000023d6dc0> { length = 8, contents = "v12@?0B8" }
"sequence" => <uint64: 0xba03d643bd007987>: 69
}>
And it's like "here's how to inspect the data but also, what's PID 6006?"
ps -p 6006 -o comm=
/System/Library/ExtensionKit/Extensions/ControlCenterSettings.appex/Contents/MacOS/ControlCenterSetting
So we connected lldb to that instead, and it was the breakpoint on CFPreferencesSetValue that led to the discovery I needed.
This was turning it "on":
/*
Claude:
CFPreferencesSetValue(key, value, appID, userName, hostName) — on ARM64 that's x0 through x4:
po $x0
po $x1
po $x2
po $x3
po $x4
*/
(lldb) po $x0
Battery
(lldb) po $x1
18
(lldb) po $x2
com.apple.controlcenter
(lldb) po $x3
kCFPreferencesCurrentUser
(lldb) po $x4
kCFPreferencesCurrentHost
And turning it "off" was passing the value 24 instead of 18. So all I needed was:
defaults -currentHost write com.apple.controlcenter Battery -int 24 # hide the icon
defaults -currentHost write com.apple.controlcenter Battery -int 18 # show the icon
From there, I asked how to detect when the laptop is plugged/unplugged, and Claude gave a few options, one of which was the Swift script in this repo. It got the on/off backwards at first, but we worked it out.
And then I asked it to create a launchd service to run it in the background, and the (first half of) this README. All the prose at the end here is written by me, but everything up to Try It was Claude.
This was kind of amazing
I built this on a lark because that icon was bugging me, and it only took maybe 20 minutes.
On one hand, against the backdrop of the AI coding hype these days, this is unremarkable. It's just a tiny script doing a tiny job.
On the other hand though, it feels like a superpower to be able to combine Claude with a debugger and customize parts of macOS that are otherwise off limits. This is the kind of skillset that would've taken ages to develop, and to be fair, I've been doing computers for a long time so I knew a bit what to ask for here.
I think that points to something that's becoming apparent with these tools. There's a sort of multiply and add effect on your existing skills. If you have zero coding skills, you can pull off some amazing stuff with Claude. If you're already an experienced developer, it multiplies what you can already do. Mere background vocabulary and some vague hand-wavy knowledge of systems that would otherwise be years-long efforts to master become accessible with a few prompts. Wild days!