Skip to content

About

🪖 불침번 — Your Mac's night-watch soldier. Keeps your Mac awake only while AI agents work. No hooks, no daemon, macOS 13+.

Topics

Resources

Contributing

Security policy

Stars

58 stars

Watchers

1 watching

Forks

Latest commit

 

History

86 Commits

Folders and files

Repository files navigation

🪖 Bulchimbeon (불침번)

Your Mac's night-watch soldier. A menu bar app that keeps your Mac awake only while your AI coding agents are actually working, then salutes "Shift over!" and lets it sleep.

No hooks, no daemon, no installed root helper. One app. macOS 13 and later. On Linux, a small daemon with a command line and a tray icon does the same job: see Linux.

Bulchimbeon (불침번) is the night-watch duty in the Korean army: someone stays up all night so everyone else can sleep. A pixel-art private takes that shift for your Mac.

🌐 한국어 · 日本語 · 简体中文 💡 Ideas / 문의 / 建议 🗳️ Which agent do you use it with? Where did you get stuck? Vote and tell us

Bulchimbeon demo

Start here

Install: brew install --cask s3onghyun/tap/bulchimbeon — or download the latest macOS zip and move it to /Applications.

First launch: the app is ad-hoc signed and not notarized by Apple yet, so macOS blocks it once:

  1. Open Bulchimbeon. macOS says it can't be opened; click Done.
  2. Go to System Settings → Privacy & Security and scroll down to Security.
  3. Next to "Bulchimbeon" was blocked, click Open Anyway, then confirm. (Since macOS 15, right-click → Open no longer bypasses this.)

System Settings, Privacy & Security: "Bulchimbeon" was blocked to protect your Mac, with the Open Anyway button

Notifications show up as "Script Editor". That is normal: until the app is notarized, macOS won't grant it notification permission of its own.

Supported agents: Claude Code · Codex · Gemini CLI · Cursor Agent · Copilot CLI · Aider · OpenCode (and your own custom agents).

Install with your AI agent

Paste this into Claude Code, Codex, Gemini CLI or another coding agent. The agent follows docs/install-for-agents.md and stops when you need to click Open Anyway.

Install Bulchimbeon (https://github.com/s3onghyun/bulchimbeon) on this computer by following
https://raw.githubusercontent.com/s3onghyun/bulchimbeon/main/docs/install-for-agents.md step by step. Show me each command before you run it.
Rules: install only from the Homebrew cask s3onghyun/tap/bulchimbeon or github.com/s3onghyun/bulchimbeon releases,
and check the SHA-256 of anything you download. No sudo for the Mac app. Never remove the quarantine attribute
(xattr) or turn off Gatekeeper (spctl): when macOS blocks the first launch, stop and let me click Open Anyway myself.
Finish by checking with me that Agent watch is ticked in the menu.

1-minute check after install

  1. Click the private in the menu bar and check that 🤖 Agent watch (auto shift) is ticked. Agent watch is on by default; if you turned it off, tick it. The app remembers your choice.
  2. Give an agent a task, for example ask Claude Code to run your tests.
  3. Within a few seconds the private in the menu bar turns his flashlight on.
  4. Open the menu. Below the welcome note (shown until you click Got it), the status line reads Bulchimbeon — On duty · 0:12 · …, and the line under it says why, such as Claude Code working.
  5. Nothing happens? Run /Applications/Bulchimbeon.app/Contents/MacOS/Bulchimbeon --diagnose to see what it detects, and tell us where you got stuck.

To see the sleep block macOS itself is holding, run pmset -g assertions. While on watch:

2026-10-31 13:17:17 +0900
Assertion status system-wide:
   BackgroundTask                 0
   ApplePushServiceTask           0
   UserIsActive                   0
   PreventUserIdleDisplaySleep    0
   SoftwareUpdateTask             0
   PreventSystemSleep             0
   ExternalMedia                  0
   PreventUserIdleSystemSleep     1
   NetworkClientActive            0
Listed by owning process:
   pid 2102(Bulchimbeon): [0x002847ba00019690] 13:31:53 PreventUserIdleSystemSleep named: "Bulchimbeon on duty: keeping AI agents awake"
Kernel Assertions: 0x100=MAGICWAKE
   id=592  level=255 0x100=MAGICWAKE creat=  mod= description=en0 owner=IOSkywalkNetworkBSDClient
  • 2026-10-31 13:17:17 +0900: when the command ran.
  • Assertion status system-wide: every kind of sleep block, with how many are held right now. PreventUserIdleSystemSleep 1 means something is keeping the Mac from idle sleep. PreventUserIdleDisplaySleep 0 means the display may still sleep, the default watch mode. With Keep display on, Bulchimbeon takes a display block instead, so PreventUserIdleDisplaySleep reads 1 and Bulchimbeon's own line below shows that kind.
  • Listed by owning process: who holds each block. pid 2102(Bulchimbeon) is the app (your pid will differ), [0x002847ba00019690] is the block's ID, and 13:31:53 is when the watch took it. That time has no date, so it can be from an earlier day and read later than the header time. PreventUserIdleSystemSleep is its kind (PreventUserIdleDisplaySleep with Keep display on), and named: "Bulchimbeon on duty: keeping AI agents awake" is the fixed name to look for. Other apps holding the Mac awake would be listed here too.
  • Kernel Assertions: macOS's own entries (here a network wake for en0), not from Bulchimbeon; yours may differ.

Captured on macOS 26.6.2, Apple Silicon, Bulchimbeon 0.11.0.

While idle (no agent working), Bulchimbeon releases its block, so pmset -g assertions | grep -i bulchimbeon prints no Bulchimbeon line.

FAQ

Why not just use caffeinate or Amphetamine? Those are great when you want a manual, fixed wake lock. Bulchimbeon starts and ends the lock around active AI-agent work, so it does not get left on overnight.

What does it inspect or send? It uses no hooks. Its only network request is the once-a-day update check, which sends nothing but its version and can be turned off. It observes only your own local process metadata to decide whether a supported agent is working; it never stores or prints process command lines or environment variables.

Can it keep a closed laptop awake? Yes, but turning on the lid-closed mode requires an administrator approval. It restores normal lid sleep when the watch ends or the app quits.

State The private When
💤 Off duty Dozing in his helmet, zzz Nothing to guard
👀 Standing by Looking around Waiting for an agent to clock in
🪖 On duty Flashlight on, on patrol The Mac won't sleep
🫡 Shift over ^ ^ salute Agents went quiet / watch time is up
🥵 Combat rest Sweating, eyes closed The Mac is overheating

🚶 Patrol mode

Sprites

The private patrols just under your menu bar with a flashlight. While standing by he looks around in the corner; at shift change he salutes with a speech bubble. Clicks pass straight through, he fades when your pointer comes close, and he stays out of full-screen apps (presentations, screen sharing, video). Size 2× / 3×, or turn him off.

🗣️ The private's lines

When He says
On watch "Won't doze off! …probably." · "Sleep tight. I've got this!"
Standing by "Polishing my rifle… standing by."
Mac heating up "Heat advisory! Hydrating 🥵" → "Combat rest! Cooling down 🧊"
Low battery "Reporting to the duty officer 🔋 Battery at 15%… out of strength, standing down."
Sleep block lost "Not AWOL! Back on post! 🫡"
You come back "Handover report! 📋"

Why

To leave an agent running overnight I used to do this:

caffeinate -d                 # ...and forget to turn it off
sudo pmset disablesleep 1     # ...and forget to turn it back on → battery at 0%

Bulchimbeon stays awake only while an agent is working and hands the Mac back to normal sleep when it's done.

Features

Feature What it does
🤖 Agent watch Detects Claude Code, Codex, Gemini CLI, Cursor Agent, Copilot CLI, Aider, OpenCode (plus your own). Only counts agents that are actually working, not sessions that are merely open. Clocks in when one starts working and hands over 5 minutes (adjustable) after everything goes quiet
🪖 Timed watch 30 min / 1 h / 2 h / 4 h / indefinitely
Watch modes Keep display on (caffeinate -d) · Display may sleep, Mac stays awake (caffeinate -i) · Stay awake with the lid closed (admin)
🔋 Battery stand-down Stands down when running on battery below your threshold (default 20%)
⚠️ Lid safety net Restores normal lid sleep (disablesleep back to off) when the watch ends or the app quits; after a crash, the next launch offers Restore lid sleep in the menu
🌡️ Heat advisory → 🔥 combat rest Warns when the Mac heats up; when it gets critical, pauses the watch to cool down and resumes automatically once it cools (no more cooking a closed Mac in a bag)
⚠️ Abandoned-post check If the sleep block disappears for any reason, it is re-engaged immediately
🕵️ Watchdog In lid-closed mode, restores normal sleep within 5 s when the app quits or the watch ends (including a timed watch running out) — no second password prompt needed; re-enabling replaces rather than stacks it
📋 Night-watch handover Come back after 10+ minutes away and get a report of what happened while you were gone
🔁 Survives restarts Remaining watch time carries over app restarts, updates and reboots (for security, a lid-closed watch comes back as a system watch for that watch only; lid mode stays selected in the menu and asks for approval again on the next watch)
🖥️ Who else is holding the Mac awake The menu shows other apps blocking sleep, e.g. "Vivaldi (video)"
🌐 Korean / English / Simplified Chinese Follows the system language by default; pick one from 언어 / Language / 语言 in the menu
📒 Duty log ~/Library/Application Support/Bulchimbeon/duty-log.txt

Compared with similar apps

Adrafinil solves the same problem and came first. The two take opposite approaches, so pick the one that fits. (As of October 2026.)

Bulchimbeon Adrafinil
How it knows an agent is working Watches from outside: the agent's caffeinate child and CPU use The agent tells it: hooks (acquire on prompt submit, release when the reply ends) plus observation
What installing changes Nothing. One app; agent config files are never touched Hooks added to agent configs (one-click install/uninstall), a user daemon, a root helper
macOS 13 and later 26.4 and later
Awake with the lid closed Admin password each time you turn it on; a watchdog guarantees the setting is restored Approved once at install, no password afterwards
Agent asks to stay awake (long builds) — CLI and MCP tool
Heat Warns even with the lid open; when it gets too hot, rests and resumes automatically once cool Force-releases on overheating while the lid is closed
Distribution Ad-hoc signed (notarization planned), personal Homebrew tap Notarized, official Homebrew cask

Choose Adrafinil if you're on macOS 26.4+ and want hooks to track turns precisely, or want agents to request wake time themselves. Choose Bulchimbeon if you're on macOS 13–15, don't want anything installed into your agent configs or system, or would rather have a private on watch.

Install

brew install --cask s3onghyun/tap/bulchimbeon

# Update — refresh the tap first, Homebrew's auto-update interval is long
brew update && brew upgrade --cask bulchimbeon

Or download the zip from Releases and move it to /Applications. macOS 13+, universal (Apple Silicon and Intel).

First launch: macOS blocks the app once; the steps are in Start here.

Verify the download (optional)

Release zips are built only in GitHub Actions and carry a build provenance attestation.

gh attestation verify Bulchimbeon-<version>.zip --repo s3onghyun/bulchimbeon
shasum -a 256 -c Bulchimbeon-<version>.zip.sha256

Updates

Once a day the app (and the Linux daemon) asks GitHub whether a newer release is out. If one is, the menu shows New version X.Y.Z — Update and Release notes; a release that fixes a serious bug is marked (recommended: important fix). Nothing is downloaded or installed until you click Update and confirm.

  • Installed with Homebrew: Terminal opens and runs brew update && brew upgrade --cask bulchimbeon in plain sight, then reopens the app.
  • Installed from the zip: the app downloads Bulchimbeon-X.Y.Z.zip and its .sha256 from that release, checks the checksum, and checks that the new app's code signature is valid, has the same signing identifier and team as the running app, uses the hardened runtime and is exactly the offered version. Only then does it replace itself and restart; the current watch carries on. If any step fails, the installed app is left as it was and the release page is offered instead. The app must sit in a folder you can write to, such as /Applications.
  • Linux: notify only. bulchimbeon status, the tray menu and bulchimbeon doctor show the update with the command for how you installed it (brew upgrade bulchimbeon-linux, sudo apt install ./….deb, sudo dnf install ./….rpm, yay -Syu bulchimbeon-bin, or the tarball), and the daemon notes it in the journal.

Privacy. The check is a single HTTPS GET https://api.github.com/repos/s3onghyun/bulchimbeon/releases/latest, at most once a day, with only User-Agent: Bulchimbeon/<version>, Accept and a fixed Accept-Language: en. No settings, duty log, agent names, identifiers or cookies are sent, and there is no telemetry. As with any web request, GitHub sees your IP address. This is the only network access Bulchimbeon makes.

To turn it off, untick Check for updates (once a day) in the menu, or:

defaults write io.github.s3onghyun.bulchimbeon checkForUpdates -bool false   # macOS
bulchimbeon config set checkForUpdates false                                  # Linux

How it works

Watch mode Under the hood
Keep display on IOPMAssertion PreventUserIdleDisplaySleep, same as caffeinate -d
Display may sleep (default) IOPMAssertion PreventUserIdleSystemSleep, same as caffeinate -i
Lid closed The above plus pmset -a disablesleep 1. Closing the lid is a hardware trigger that assertions can't block

Agent detection

Bulchimbeon scans your own user's processes with libproc and matches agents by executable name, argv[0], install path or script path. Never by substring (Claude Code hooks run node -e "...'.claude'...", which would match).

Install method What the process looks like Matched by
Claude Code native installer executable ~/.local/share/claude/versions/2.1.296 (named after the version!) argv[0] = claude, or the install path
Homebrew / IDE-bundled binary .../claude, /opt/homebrew/bin/codex executable name
npm global install node /opt/homebrew/bin/claude script name
npx / package path node .../@anthropic-ai/claude-code/cli.js package path
bun / deno bun .../@google/gemini-cli/... package path

Supported: Claude Code · Codex · Gemini CLI · Cursor Agent · Copilot CLI · Aider · OpenCode

Only while working. People keep several Claude Code sessions open, so counting open sessions would never let the Mac sleep. An agent counts as working when it shows its own recent activity:

Signal Why (measured on a real machine)
The agent plus processes it started in the last 10 minutes use ≥ 2% CPU, averaged over 60 s Covers replies and tool runs like builds and tests. Idle sessions sit at 0–0.1% (0.6–0.9% on Linux). A 5-second sample made periodic background jobs look busy forever
A process running longer than 10 minutes counts only if it alone uses ≥ 5% CPU Long builds still count. MCP servers (~0.3%) and hook pollers (~1%) do not, and no longer add up across a session
A caffeinate child counts when another child started in the last 10 minutes runs next to it Claude Code spawns caffeinate -i -t 300 while a turn is in progress, but a Stop hook that waits for hours keeps the turn open. Tool calls and command hooks time out after 10 minutes by default
An agent started under another agent (a claude -p helper run by a hook) is judged on its own Its CPU is not added to the session that started it
Headless sessions with no tools (--tools=) are ignored Plugins such as claude-mem keep helper sessions running

To go back to the old behaviour, where any open agent process keeps the Mac awake, turn on Agent watch settings ▸ Count idle sessions as working (agentWatchCountsIdle). The 5-minute grace before handover is in the same submenu. --diagnose shows, for each agent, which of these rules decided it.

Check what gets detected on your machine, or add an agent that isn't on the list:

/Applications/Bulchimbeon.app/Contents/MacOS/Bulchimbeon --diagnose
defaults write io.github.s3onghyun.bulchimbeon customAgents -array goose amp

Agents running inside Docker or on a remote host over SSH are not processes on this Mac, so they aren't detected.

pmset -g assertions | grep Bulchimbeon   # is it on watch right now?

The duty logic lives in a pure state machine (DutyEngine) with no system calls, tested scenario by scenario with a fake clock. Design notes, including signals we tried and rejected: docs/design.md.

Linux

Bulchimbeon also runs on Linux as a small daemon with a command line and a tray icon. It uses the same engine as the macOS app: a Go port that must pass the same golden test cases (testdata/engine) as the Swift core, in CI on every pull request. Tested on Ubuntu 24.04 (systemd 255). It needs systemd and logind. While on duty it blocks automatic suspend with your desktop's own inhibitor, or with logind's when there is no desktop (below).

Install

With Homebrew on Linux (no root; brew upgrade brings new versions):

brew install s3onghyun/tap/bulchimbeon-linux
brew services start bulchimbeon-linux    # systemd user unit, started at login

Pick one install method. If you use Homebrew, do not also install the tarball or enable the .deb or .rpm unit (systemctl --user enable bulchimbeon), or two daemons will run.

Or download from Releases: a tarball or a .deb for amd64 or arm64, or, from 0.9.0, an .rpm for x86_64 or aarch64.

# Tarball: no root needed. Installs ~/.local/bin/bulchimbeon and a systemd user unit, then starts it
tar -xzf bulchimbeon_<version>_linux_amd64.tar.gz
cd bulchimbeon_<version>_linux_amd64 && ./install-linux.sh

# or the .deb: installs /usr/bin/bulchimbeon and the user unit; each user then enables it
sudo apt install ./bulchimbeon_<version>_amd64.deb
systemctl --user enable --now bulchimbeon

# or the .rpm (Fedora and others, from 0.9.0): the same files; each user then enables it
sudo dnf install ./bulchimbeon-<version>-1.x86_64.rpm
systemctl --user enable --now bulchimbeon

Coming soon, not published yet: a Fedora COPR repository (dnf copr enable) and the AUR package bulchimbeon-bin. Until then, use the .rpm on Fedora, and on Arch the tarball, or makepkg -si in packaging/aur/bulchimbeon-bin of a checkout.

Verify the download (optional): sha256sum -c SHA256SUMS --ignore-missing, and gh attestation verify <file> --repo s3onghyun/bulchimbeon.

Uninstall with brew services stop bulchimbeon-linux && brew uninstall bulchimbeon-linux, ./install-linux.sh --uninstall, or systemctl --user disable --now bulchimbeon followed by sudo apt remove bulchimbeon or sudo dnf remove bulchimbeon. Your settings (~/.config/bulchimbeon) and duty log (~/.local/state/bulchimbeon) are kept.

To keep the service running while you are logged out, run loginctl enable-linger once.

Use

bulchimbeon status                       # on duty or not, and why
bulchimbeon doctor                       # check the desktop session, systemd, logind and polkit
bulchimbeon config set agentGraceMinutes 10
bulchimbeon config set customAgents goose amp
bulchimbeon config set language ko       # ko | en | zh | system (default: follows LANG)
journalctl --user -u bulchimbeon -f      # the daemon's log
$ bulchimbeon status
🪖 Bulchimbeon — on duty for 0:35 (agent watch)
Lock:      blocking auto-suspend via org.gnome.SessionManager · confirmed by the session
Why:       Claude Code pid 1174857 11.7% (own + recent processes CPU ≥ 2.0%, 60 s average)
Agents:    Claude Code ×7 open, 1 working
Handover:  5 min after the last agent goes quiet (agentGraceMinutes)
Power:     battery: none · heat: normal (coretemp 49 °C, critical 105 °C) · lid: not handled (closing it still suspends)

bulchimbeon run is the daemon itself; the user unit runs it. It always does agent watch: the same detection rules, a 5-second check, engage when an agent and the processes it started in the last 10 minutes use at least 2% CPU (60-second average; Linux has no caffeinate signal), hand over after the grace time.

Tray icon

bulchimbeon tray puts the private in your desktop panel, in the same poses as on the Mac menu bar and animated the same way: asleep when the daemon is off, looking around while it waits for an agent, flashlight on while on duty, sweating through a combat rest. Hover for the full status. The menu shows every line of bulchimbeon status in the same order, then a 🧥 Uniform picker that sets uniformTheme, and Quit tray, which closes the icon only: the daemon keeps watch without it.

The .deb and .rpm start it with every desktop session through an XDG autostart entry, /etc/xdg/autostart/bulchimbeon-tray.desktop; install-linux.sh puts the same entry in ~/.config/autostart/. With Homebrew, add bulchimbeon tray to your desktop's startup applications. Where there is nothing to show it in (SSH, a headless machine, a desktop without a tray), or for a user who has never started the daemon, it logs one line and exits.

It is a StatusNotifierItem, the tray protocol KDE, XFCE and the GNOME AppIndicator extension speak:

Desktop What it needs
XFCE 4.16 or later the Status Tray panel item (systray), which the default panel has
XFCE 4.14 or older the xfce4-statusnotifier-plugin panel item
GNOME the AppIndicator extension (gnome-shell-extension-appindicator, on by default in Ubuntu)
KDE Plasma nothing. Off duty or standing by, the item is Passive, so Plasma keeps it in the tray's hidden icons until the private goes on duty
bulchimbeon tray &                          # show it now, in this desktop session
bulchimbeon tray --lang ko                  # menu in Korean (ko | en | zh), whatever the language setting says
bulchimbeon config set uniformTheme navy    # default | navy | arctic | desert: same as the Uniform menu

To stop it starting at login, turn it off in your desktop's session and startup settings, or remove the autostart file.

How it works on Linux

Linux
Agents Your own processes from /proc, with the same matching rules and the same privacy limits as on macOS
Sleep lock The desktop session's suspend inhibitor, or a logind sleep inhibitor without one (per desktop). Never an idle lock. If the daemon dies, even with kill -9, the lock goes with it
Battery stand-down /sys/class/power_supply
Heat hwmon (coretemp, k10temp, …) or thermal zones: heat advisory 15 °C below the critical temperature, combat rest 5 °C below
Duty log ~/.local/state/bulchimbeon/duty-log.txt, same format as on macOS

The sleep lock on each desktop

Session Lock while on duty Still works
GNOME (Ubuntu, Fedora), Cinnamon, MATE, Budgie org.gnome.SessionManager.Inhibit with the suspend flag only the screen dims and locks; your own suspend from the menu. GNOME 46 (Ubuntu 24.04) backs any suspend inhibitor with a logind block lock of its own, so systemctl suspend in a terminal asks for -i
KDE Plasma org.freedesktop.PowerManagement.Inhibit, which PowerDevil applies to automatic suspend only the screen dims, turns off and locks; manual suspend
XFCE by default a logind sleep inhibitor, as without a desktop, which does not stop xfce4-power-manager's automatic suspend on any systemd version (block and block-weak both let your own user's suspend through). bulchimbeon config set xfceKeepAwake true switches to org.freedesktop.PowerManagement.Inhibit the screen blanks and locks; manual suspend. With xfceKeepAwake, automatic suspend is blocked too, but XFCE's power manager treats any inhibitor like presentation mode, so the screen then stays on and unlocked while on duty
No desktop: headless, SSH only, sway, Hyprland, other compositors a logind sleep inhibitor, block-weak on systemd 257 and later, block on 256 and older, held by a systemd-inhibit child tied to a pipe your own suspend. It stops logind's IdleAction and other users' suspend requests. On systemd 256 and older, systemctl suspend in a terminal asks for -i

Why not logind everywhere: on systemd 256 and older (Ubuntu 24.04, Debian 12) logind ignores a block inhibitor for requests from the user who holds it, and GNOME's, XFCE's and Cinnamon's power managers ask logind to suspend as you. Measured on Ubuntu 24.04 GNOME: gsd-power's automatic suspend request went through logind while Bulchimbeon held its lock. On systemd 257 and later a logind block lock does stop them, but it also refuses your own manual suspend.

The daemon looks for the desktop on your session bus, the user bus that systemd --user gives its services. Log in to a desktop while it is on duty and the lock moves to the desktop within one check (5 seconds); log out and it moves back to logind. The desktop drops its inhibitor when the daemon's bus connection closes, so a crash releases it. bulchimbeon status shows which lock is held and why; bulchimbeon doctor tries each one.

Limits

  • Closing the lid still suspends. By default logind ignores inhibitors for the lid switch (LidSwitchIgnoreInhibited=yes), and Bulchimbeon never changes logind's settings.

  • Over SSH or on a headless machine, the sleep block needs a polkit rule. By default polkit allows a sleep block inhibitor only from a local login (someone at the machine's screen). From an SSH session, or as a user service on a machine nobody logs into locally, logind refuses it, and bulchimbeon status says plainly that sleep is not blocked. To allow it for one user, save this as /etc/polkit-1/rules.d/50-bulchimbeon.rules (owner root, mode 0644), with your user name; bulchimbeon doctor prints it with your name filled in:

    // Bulchimbeon: let YOUR-USER-NAME hold a logind "sleep" block inhibitor from an SSH or
    // systemd --user session (polkit's default only allows it from a local session).
    // Grants nothing else. Remove this file to undo.
    polkit.addRule(function (action, subject) {
      if (action.id == "org.freedesktop.login1.inhibit-block-sleep" && subject.user == "YOUR-USER-NAME") return polkit.Result.YES;
    });

    It grants only org.freedesktop.login1.inhibit-block-sleep to that user; delete the file to undo. Bulchimbeon never runs this for you. A desktop session does not need it: its own inhibitor needs no polkit permission. Only the logind lock, used without a desktop, does.

  • sway, Hyprland and other compositors without a desktop power manager get the logind lock only. It does not stop an idle daemon that suspends as you (swayidle or hypridle running systemctl suspend), because logind lets your own suspend through. Bulchimbeon holds no idle lock there either: on those compositors one would also stop the screen from locking.

  • On XFCE, automatic suspend is not blocked by default. xfce4-power-manager treats any suspend inhibitor like presentation mode and keeps the screen on and unlocked, so Bulchimbeon leaves the screen lock alone and holds only the logind lock, which xfce4-power-manager's own suspend gets past on any systemd version. bulchimbeon status and doctor say so. To block it anyway, accepting that the screen stays on and unlocked while on duty: bulchimbeon config set xfceKeepAwake true.

  • The desktop inhibitor covers your own session. Another user's session on the same machine (fast user switching) can still suspend it. A desktop whose session bus is private (started with dbus-launch, without dbus-user-session) is invisible to the service, which then keeps the logind lock.

  • Only the "Display may sleep" watch mode exists on Linux (dutyMode is ignored), there is no patrol mode, and notifications go to the journal.

  • Windows is planned: the platform layer is in internal/platform.

Security

The app runs pmset with admin rights and reads other processes' metadata, so it has its own threat model: SECURITY.md.

  • No network access except the once-a-day update check, which sends only the app version and can be turned off; no third-party dependencies
  • Updates install only after you click: Homebrew installs go through brew in a visible Terminal, zip installs are replaced only after the checksum and code-signature checks pass
  • No shell-injection surface: absolute paths, minimal environment, and only an app PID and a safely quoted app-owned stop-flag path enter the admin command
  • The root lid watchdog has no deadline to extend; the app owns all watch timing. A user-owned, non-world-writable empty stop flag can only make it run pmset -a disablesleep 0; it never reads the flag, follows it elsewhere, or writes it
  • Never stores or prints other processes' arguments or environment; never reads AI transcripts
  • Hardened runtime, SHA-256 checksums and build provenance for releases, CI actions pinned by commit SHA

Development

Only the Command Line Tools are needed, no Xcode. The Linux daemon is a Go module (Go 1.27 or later): go test ./... and go build ./cmd/bulchimbeon.

make test       # unit + scenario tests
make app        # dist/Bulchimbeon.app (ad-hoc signed)
make run        # build and launch
make icons      # regenerate the app icon and preview after editing sprites

# launch and start agent watch right away
open dist/Bulchimbeon.app --args --agent-watch

# print the engine's effects to stderr (for tracing)
BULCHIMBEON_DEBUG=1 dist/Bulchimbeon.app/Contents/MacOS/Bulchimbeon

Want to help? Start with CONTRIBUTING.md.

Release (maintainer)

git tag vX.Y.Z && git push origin vX.Y.Z   # → the Release workflow builds, attests and uploads
./scripts/bump-cask.sh X.Y.Z               # → verifies checksums and provenance, then updates the tap's cask and Linux formula
./scripts/bump-aur.sh X.Y.Z                # → same checks, then updates packaging/aur for the AUR (packaging/aur/README.md)
# Fedora COPR, once set up: copr-cli buildscm ... --commit vX.Y.Z (packaging/rpm/README.md)

Sprites are ASCII grids in Sources/BulchimbeonCore/Sprites.swift: one character per pixel, drawn as vector rectangles so both the 16 pt menu bar icon and the 1024 px app icon stay crisp.

Menu-bar animation and power

The menu-bar sprite advances at 2 frames per second while it can be seen. It stops completely when the screen is locked, the displays sleep, or AppKit reports the status item's window as fully occluded; it runs at 0.67 fps in Low Power Mode. The timer has tolerance, and a single-frame pose schedules no timer at all, so it creates no redraw.

One AppKit NSStatusItem is replicated by macOS to each menu bar. AppKit does not expose an independently drawable status item or active display for those copies, so Bulchimbeon cannot animate just one display without replacing the standard menu-bar item. The visible animation therefore remains identical on every active display.

Measured on 2026-11-06 on an M5 Pro on AC with the built-in panel plus two LG 4K displays: top reported 2.04% CPU and 167 context switches/s before this change, 1.58% CPU and 186 context switches/s after it (five 2-second samples, patrol mode enabled). top on this macOS release has no per-process wakeups counter, so context switches are recorded as the available wakeup proxy; normal visible animation is intentionally not claimed as a speed-up. The saving is the zero scheduled menu-bar frames while it is not visible (rather than 2/s), and 67% fewer frames in Low Power Mode.

Roadmap

Tracked as milestones. 1.0 means settings keys, command-line flags and the duty log format stop changing: see the compatibility promise (in Korean; the tables are self-explanatory) and the CHANGELOG.

  • Pixel-art private, patrol mode, Korean/English/Simplified Chinese UI
  • Detect working vs. idle agents
  • Homebrew cask (personal tap)
  • Apple notarization and the official Homebrew cask
  • Privileged helper to remove the admin prompt in lid-closed mode
  • Unlockable skins: Joseon night watchman, scholar

Contributing

Bug reports ("🚨 soldier misbehaving"), agent requests, translation fixes, and ideas for lines or sprites are all welcome. Issues and PRs in English, Korean, Japanese or Chinese are fine. Look for good first issue to get started, and read CONTRIBUTING.md.

Caution

Heavy, long-running work with the lid closed makes the Mac warm. Plug in power when you can.

License

MIT

About

🪖 불침번 — Your Mac's night-watch soldier. Keeps your Mac awake only while AI agents work. No hooks, no daemon, macOS 13+.

Topics

Resources

Contributing

Security policy

Stars

58 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages