[Bug] macOS sleep inhibitor ignores power source: caffeinate re-asserts on battery until hard shutdown

Status Open
Reported on v2.1.226
Maintainer reply None cached
Activity 1 comment · opened Aug 9, 2026

Summary

The macOS sleep inhibitor spawns caffeinate -i -t 300 and re-asserts it every 4 minutes regardless of power source. On battery this prevents idle sleep until the machine hard-powers-off at 0%, losing the session — and because macOS writes the hibernation image only at the moment of sleep, a machine that never sleeps also never hibernates, so there is no safe-sleep fallback.

Related: #21432 requests an opt-out setting (open, enhancement). This report is about the destructive default rather than the missing switch, and adds implementation detail from the shipped bundle. #64522 and #28046 cover overlapping symptoms and are closed. Happy for this to be folded into #21432 if maintainers prefer a single thread — I would have added it there as a comment, but the account filing this cannot comment on repositories outside its installation.

Environment: Claude Code 2.1.226, macOS Darwin 25.5.0, Apple Silicon.

Reproduction

  1. Run one or more Claude Code sessions on battery power.
  2. pmset -g assertions | grep -A2 caffeinate
pid 98815(caffeinate): PreventUserIdleSystemSleep "caffeinate command-line tool"
        Details: caffeinate asserting for 300 secs
        Timeout will fire in 289 secs Action=TimeoutActionRelease

Observed: charge went 9% → 7% → 6% with six inhibitors alive and the machine refusing to idle-sleep. The assertion is re-created every 4 minutes for as long as any session is busy, so killall caffeinate is not an effective workaround.

Implementation detail

From the 2.1.226 bundle:

function n8m(){ if(Clv) return null;
  let e = String(vlv);                        // vlv = 300
  if (Xt() === "macos") return ["caffeinate", ["-i","-t", e]];
  return null }

// acquire/release, refcounted on the UI busy state:
Qr.useEffect(() => { if (_a === "busy") return t8m(), () => r8m() }, [_a])

vlv = 300      // caffeinate -t, seconds  (dead-man's switch)
Elv = 240000   // respawn interval, ms    (kill + respawn every 4 min while busy)
wlv = 30000    // idle grace before SIGKILL, ms
Clv = !1       // "disabled" flag

Three observations:

A disable flag already exists and is hardcoded. Clv is assigned exactly once in the whole bundle, to false. n8m() already returns null when it is true, and every call site handles null (that is the non-macOS path). So gating the inhibitor needs no new plumbing.

The "leak" in #28046 is per-session refcounting, not accumulation. Each session holds its own inhibitor and the 4-minute respawn means the observed process is always under ~4 min old. With 6 concurrent sessions I saw exactly 6 caffeinate processes, ages 0:12 / 0:39 / 1:23 / 3:13 / 3:42 / 3:53 — that sawtooth is Elv. Parentage confirms the source is the CLI itself: caffeinate is a child of claude, and grep -rn caffeinate ~/.claude/ ~/.zshrc was empty, ruling out user hooks or a wrapper.

The behavior is undocumented. No sleep or power-management setting or env var appears in the settings documentation, so there is no discoverable way for a user to learn this is happening.

Expected behavior

Keeping the machine awake for a long unattended run makes sense on AC. On battery it produces a dead-battery hard shutdown, which is strictly worse than the sleep it was preventing. For comparison, third-party keep-awake utilities (Amphetamine, Caffeine, Cofejn) release their assertion when the machine switches to battery — AC-awareness is the normal expectation for this class of tool.

Suggested fix

  1. Gate n8m() on power source — return null on battery. Fixes the destructive case with no configuration and no change to the AC scenario the feature exists for.
  2. Wire Clv to a settingdisableCaffeinate in settings.json and/or CLAUDE_CODE_DISABLE_CAFFEINATE=1, matching the existing CLAUDE_CODE_DISABLE_* family. This is the ask in #21432.
  3. Document the behavior either way.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗