Skip to content

winzamark123/Adderail

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Adderail

Adderail is a macOS utility that keeps a Mac awake only while a local AI coding agent is actively working.

The product promise:

Let coding agents keep running, including during closed-lid workflows when explicitly enabled, then restore normal sleep behavior automatically when the agent stops.

The name combines "derail" with "AI" and uses adderail.com as the domain-owned naming base.

Naming convention:

  • Adderail is the project, app, package, and Swift target naming style.
  • adderail is the lowercase command-line executable that hooks invoke.

Current Direction

This project should start as a proof of concept, not a polished app.

The first thing to prove is not the menu bar UI. The first thing to prove is that an agent lifecycle event can reach a persistent user-level controller, create or refresh a short-lived lease, keep the Mac awake while that lease is active, and reliably restore normal sleep behavior when the lease ends or expires.

The standalone caffeinate-managed CLI is not a goal. It is too trivial and does not match the intended runtime model. The lease protocol from the original MVP 0 remains important, but it should be implemented through a controller-first architecture:

agent hook/extension -> adderail CLI -> XPC -> user-level controller -> IOPMAssertion

Adderail should be installed as deterministic lifecycle integrations, not exposed as a model-chosen tool. A tool is something the agent may decide to call. A hook or extension event is something the agent runtime invokes because lifecycle activity happened. Keeping the Mac awake must not depend on the model remembering to call a tool.

The CLI should not be the source of truth for power state, and the lease store should not be used as a command bus. The controller owns lease state. The store is only for recovery, diagnostics, and status snapshots.

No polling loop should be used to discover lease changes. CLI commands should notify the controller directly over IPC. Timers are still appropriate for scheduling the next lease expiry.

Current implemented integrations:

  • Claude Code through adderail install claude, user-level Claude hooks, and adderail hook claude.
  • Codex through adderail install codex, user-level Codex hooks in ~/.codex/hooks.json, and adderail hook codex.
  • Pi through adderail install pi and an Adderail-owned Pi extension in ~/.pi/agent/extensions/adderail/index.ts.

Development Workflow

Start with Swift Package Manager unless Xcode becomes useful for a specific Apple-platform task.

For the controller-first proof, SwiftPM is enough for the core code:

Package.swift
Sources/
  AdderailCLI/
  AdderailController/
  AdderailShared/

The controller executable uses swift-service-lifecycle only for process lifecycle scaffolding: start the XPC listener, wait for graceful shutdown signals, and invalidate the listener on exit. Lease state, snapshots, and power assertions remain explicit controller-owned logic.

The full Xcode toolchain should remain installed because later work needs Apple signing, entitlements, app bundles, SMAppService, XPC, helper daemons, and notarization. The Xcode editor UI is optional for early work.

A future Xcode project or workspace should wrap or consume the same local package code. It should not require porting the implementation into a separate codebase.

Recommended editor/build posture:

  • Use Zed or another editor for most code editing.
  • Use swift build and swift test for shared logic and CLI/controller iteration.
  • Use .build/debug/adderail install claude to copy the current CLI/controller binaries into user application support storage, bootstrap the user LaunchAgent, and install Claude Code hooks.
  • Use .build/debug/adderail install codex to copy the current CLI/controller binaries into user application support storage, bootstrap the user LaunchAgent, and install Codex hooks. Then restart Codex, run /hooks, and trust the Adderail hooks.
  • Use .build/debug/adderail install pi to copy the current CLI/controller binaries into user application support storage, bootstrap the user LaunchAgent, and install the Adderail Pi extension.
  • Use .build/debug/adderail uninstall claude, .build/debug/adderail uninstall codex, or .build/debug/adderail uninstall pi to remove provider integrations. The shared user LaunchAgent is removed only when no known integrations remain.
  • Use adderail status --json, simulated adderail hook claude / adderail hook codex stdin payloads, Codex /hooks, and Pi /reload after install pi to verify XPC, lease, snapshot, expiry, and normal-awake behavior.
  • After changing the generated Pi extension or Claude/Codex hook behavior, rerun adderail install pi, adderail install claude, or adderail install codex to update installed integration files.
  • Add xcodebuild and Xcode project metadata when app bundles, LaunchAgent registration through SMAppService, privileged helpers, signing, or notarization require it.
  • Open Xcode when it materially simplifies signing, capabilities, bundle embedding, or preview/debug workflows.

MVP Roadmap

MVP 1: controller-first normal-awake proof

Goal: prove the real local runtime shape without privileged closed-lid behavior.

Deliverables:

  • Swift command-line tool named adderail.
  • Persistent user-level controller executable named adderail-controller.
  • Shared lease/status models in AdderailShared, with live lease management owned by AdderailController.
  • Controller XPC interface.
  • LaunchAgent plist for the controller with a Mach service name.
  • Commands:
    • adderail begin --provider claude --session <id>
    • adderail heartbeat --provider claude --session <id>
    • adderail end --provider claude --session <id>
    • adderail status --json
  • Controller-owned lease state.
  • Recovery snapshot under user-owned application support storage.
  • TTL expiry using scheduled timers, not polling.
  • Normal idle-sleep prevention through IOPMAssertion.
  • Manual test flow that simulates Claude hooks, Codex hooks, and Pi extension behavior.
  • Hook/event mapping documented for Claude Code, Codex, and Pi.

Success criteria:

  • The controller can run as a user-level process.
  • The CLI sends lifecycle events to the controller over XPC and exits quickly.
  • A lease can be started from the CLI.
  • A heartbeat refreshes the lease TTL.
  • Repeated begin and heartbeat calls are idempotent for the same provider/session pair.
  • Ending the lease restores normal sleep behavior when no active leases remain.
  • Expired leases are cleaned up by the controller automatically.
  • The controller holds a normal awake assertion while at least one lease is active.
  • The controller releases the awake assertion when the last lease ends or expires.
  • adderail status --json reports controller state and active leases.
  • Controller restart drops expired leases and avoids restoring stale awake state indefinitely.
  • No command requires sudo.
  • No command prompts for a password during normal agent execution.
  • If the controller is unavailable, the CLI fails clearly and non-interactively.

Important constraints:

  • Neither Claude Code nor Codex should be treated as having a guaranteed native periodic heartbeat event.
  • Adderail's heartbeat command is the lease-refresh protocol.
  • Provider hooks can invoke heartbeat when useful lifecycle events occur.
  • The lease store is not IPC. The CLI must not mutate it directly.
  • Shared lease/status types do not imply shared state ownership. The controller owns live lease mutation and expiry.
  • Do not introduce closed-lid pmset behavior in this milestone.

MVP 2: agent hook integration

Goal: bind the controller-backed CLI to real agent lifecycle hooks for normal awake mode.

Deliverables:

  • Claude Code hook entrypoint through adderail hook claude.
  • Claude Code hook installer through adderail install claude.
  • Claude Code hook uninstaller through adderail uninstall claude.
  • Pi extension installer through adderail install pi.
  • Pi extension uninstaller through adderail uninstall pi.
  • Codex hook entrypoint through adderail hook codex.
  • Codex hook installer through adderail install codex.
  • Codex hook uninstaller through adderail uninstall codex.
  • Hook repair and diagnostics commands.
  • Manual hook simulation documentation.

Success criteria:

  • Claude Code hooks invoke adderail hook claude.
  • adderail hook claude maps hook JSON to begin, heartbeat, and end requests over XPC.
  • Pi lifecycle events invoke adderail begin, adderail heartbeat, and adderail end through an Adderail-owned Pi extension.
  • Codex hooks invoke adderail hook codex.
  • adderail hook codex maps hook JSON to begin and heartbeat requests over XPC.
  • Hook commands are fast and non-interactive.
  • Runtime hook/extension failures fail open, are logged or surfaced as non-blocking UI warnings, and do not block the agent runtime.
  • Manual install/uninstall failures fail loudly.
  • The integration does not rely on the model choosing to call an Adderail tool.
  • Normal awake mode works through real agent activity while the lid is open.

Claude preferred hook mapping:

UserPromptSubmit -> begin
PreToolUse       -> heartbeat, with tool timeout-aware TTL when available
PostToolBatch    -> heartbeat
PostToolUseFailure -> heartbeat
Stop             -> end or shorten TTL when background work may remain
StopFailure      -> end
SessionEnd       -> end

Provider hooks are hints. Lease TTL cleanup is the safety net.

MVP 3: closed-lid helper proof

Goal: add the dangerous root-required part only after controller-backed lease behavior is already proven.

Deliverables:

  • Privileged helper daemon installed through Apple's ServiceManagement APIs.
  • Tiny XPC API between the user-level controller and helper.
  • Helper toggles /usr/bin/pmset -a disablesleep 1 only while closed-lid leases are active.
  • Helper restores /usr/bin/pmset -a disablesleep 0 when Adderail-owned leases end, expire, or recover from stale state.
  • Ownership marker under /Library/Application Support/Adderail/.

Success criteria:

  • First-time setup can request admin approval intentionally.
  • Runtime hook commands never request admin approval.
  • No hook calls sudo.
  • Killing the app, controller, or helper does not leave the Mac permanently sleep-disabled.
  • The helper exposes no generic command execution API.
  • If the helper is not installed, hooks fail clearly or fall back to normal awake mode.

MVP 4: menu bar UI and packaging

Goal: provide a minimal but trustworthy user surface and make setup manageable.

Deliverables:

  • Minimal macOS menu bar app.
  • Menu bar status: inactive, active lease, controller unavailable, helper missing, closed-lid mode active, stale state restored.
  • Setup controls for LaunchAgent/controller registration, helper approval, and hook installation.
  • Uninstall and restore controls.
  • Diagnostics view.
  • Packaging/signing path suitable for local installation first, public distribution later.

Success criteria:

  • The user can see why the Mac is awake.
  • The user can remove hooks and restore power state.
  • Runtime remains automatic after setup.
  • The app/controller/helper relationship is understandable and debuggable.

Architecture

flowchart TD
    A["Claude hooks / Codex hooks / Pi extension"] --> B["adderail CLI"]
    B -->|"XPC"| C["User LaunchAgent/controller"]
    D["Menu bar app"] --> C
    C --> E["Controller-owned lease manager"]
    E --> F["Recovery snapshot"]
    E --> G["Expiry timer"]
    C --> H["Normal awake engine"]
    H --> I["IOPMAssertion"]
    C --> J["Privileged helper via XPC"]
    J --> K["pmset disablesleep 1/0"]
Loading

Mental model

This is closer to a small local operating-system utility than a web app.

The repository may look like a monorepo, but the runtime model is different:

  • Swift Package Manager or Xcode organizes code into targets.
  • Targets build products.
  • Some products are runnable executables.
  • Each executable has its own process, code signature, entitlements, and privilege level.
  • Shared code is compiled into other products; it does not run by itself.

xcodebuild builds products. It does not run every product together in parallel.

At runtime:

  • The CLI runs only when a hook or user invokes it.
  • The user-level controller is the persistent process that owns active lease state.
  • The menu bar app is a user surface and setup/diagnostics tool.
  • The privileged helper runs as a root LaunchDaemon when registered and started by macOS.
  • XPC is the RPC-like boundary between the CLI and controller, and later between the controller and privileged helper.

Hook Integration Model

Adderail should install lifecycle integration configuration for supported agents.

For Claude Code and Codex, that means hook configuration. For Pi, that means an Adderail-owned TypeScript extension. In all cases, the integration invokes the adderail CLI automatically on lifecycle events. The agent model should not need to know Adderail exists.

Avoid this:

agent model decides to call an Adderail tool

Prefer this:

agent runtime event -> installed hook/extension -> adderail CLI -> XPC -> user-level controller

Claude Code

Claude Code has mature lifecycle hooks. Users install the integration with:

adderail install claude

That command installs the user LaunchAgent/controller and writes Claude Code command hooks that invoke:

adderail hook claude

adderail hook claude reads Claude Code's hook JSON from stdin and maps lifecycle events to controller lease commands using Claude's session_id as the lease session. Runtime hook mode fails open: if parsing, XPC, or the controller fails, it logs and exits successfully so Claude Code is not blocked. When a normal Stop event ends the lease, the hook emits a user-visible systemMessage: Released adderail (agent finished).

Users remove the integration with:

adderail uninstall claude

The current mapping is:

  • UserPromptSubmit: begin or refresh a turn lease.
  • PreToolUse: heartbeat before a potentially long tool call. If Claude provides tool_input.timeout, Adderail extends the TTL to cover that timeout plus a buffer.
  • PostToolBatch: heartbeat once after a batch of parallel tools resolves.
  • PostToolUseFailure: heartbeat after a failed tool when Claude may continue working.
  • Stop: end the turn lease when background_tasks and session_crons are present and empty; otherwise refresh with a bounded TTL.
  • StopFailure: end the lease.
  • SessionEnd: end the lease.

PostToolUse is not a true periodic heartbeat. If one tool call runs for a long time, no post-tool event arrives until it finishes. PreToolUse helps by refreshing before the long operation begins, and TTL expiry remains the fallback.

Pi

Pi has native TypeScript extensions with lifecycle events. Users install the integration with:

adderail install pi

That command installs the user LaunchAgent/controller and writes an Adderail-owned extension at:

~/.pi/agent/extensions/adderail/index.ts

The extension listens to Pi lifecycle events and invokes the installed adderail CLI. It does not hold power assertions itself; it only bridges Pi activity into the controller-owned lease protocol.

The current mapping is:

  • agent_start: begin a Pi lease.
  • tool_execution_start: heartbeat before tool execution, with timeout-aware TTL when the tool args include a numeric timeout.
  • tool_execution_end: heartbeat after tool execution.
  • agent_end: end the Pi lease, clear the Pi status UI, and notify Released adderail (agent finished).
  • session_shutdown: end the Pi lease and clear Pi status UI.

Users remove the integration with:

adderail uninstall pi

While active, the Pi footer status shows awake. If the extension cannot reach Adderail during active work, it shows unavailable and warns without blocking Pi.

After install or uninstall, restart Pi or run /reload so Pi reloads extensions.

Codex

Codex has user-level command hooks. Users install the integration with:

adderail install codex

That command installs the user LaunchAgent/controller and writes Adderail-owned Codex hooks at:

~/.codex/hooks.json

Those hooks invoke:

adderail hook codex

adderail hook codex reads Codex's hook JSON from stdin and maps lifecycle events to controller lease commands using Codex's session_id as the lease session. Runtime hook mode fails open: if parsing, XPC, or the controller fails, it logs and exits successfully so Codex is not blocked.

Codex requires non-managed command hooks to be reviewed and trusted before they run. After installing, restart Codex, run /hooks, and trust the Adderail hooks.

Users remove the integration with:

adderail uninstall codex

The current mapping is:

  • UserPromptSubmit: begin or refresh a turn lease.
  • PreToolUse: heartbeat before a tool call with a conservative long-tool TTL.
  • PostToolUse: heartbeat after a tool call.
  • Stop: shorten the lease with a bounded TTL instead of ending immediately, because other stop hooks may continue the turn.
  • SessionStart: ignored for active-work leases.

Do not treat "Codex session exists" as "Codex is actively working." Use turn and tool events for active leases.

Codex hook behavior is less mature than Claude Code and Pi. Known risks to design around:

  • hook configuration edits may require a session restart;
  • hooks must be reviewed and trusted through Codex /hooks before they run;
  • codex exec has had hook-dispatch gaps, so interactive Codex sessions are the preferred manual verification path;
  • stop hooks may not fire for every abnormal stop path;
  • session-start behavior may differ between cold start, resume, and soft restart.

Components

CLI bridge

The CLI is the external integration surface for agents.

Rules:

  • Fast.
  • Non-interactive.
  • Predictable exit codes.
  • Never calls sudo.
  • Never prompts for a password during begin, heartbeat, or end.
  • Talks to the user-level controller over XPC.
  • Does not write the lease store directly.
  • Does not directly hold power assertions.

The CLI is also the setup and hook target. User-facing setup commands mutate external configuration intentionally and should fail loudly:

adderail install claude
adderail install codex
adderail install pi
adderail uninstall claude
adderail uninstall codex
adderail uninstall pi

These provider integrations share the same user LaunchAgent/controller. Uninstalling one provider removes its bridge and removes the controller only when no known integrations remain.

Runtime hook commands should be safe for hooks to call repeatedly and should fail open:

adderail hook claude
adderail hook codex
adderail hook claude

The hook entrypoint maps provider events to repeated begin, heartbeat, and when supported end requests. Repeated begin and heartbeat calls should be idempotent for the same provider/session pair.

User-level controller

The controller is the product brain.

Responsibilities:

  • Expose the user-level XPC API.
  • Manage the XPC listener lifecycle through swift-service-lifecycle.
  • Track active leases.
  • Deduplicate sessions.
  • Apply TTL expiry.
  • Schedule the next expiry timer after lease changes.
  • Persist recovery snapshots after state changes.
  • Hold normal power assertions.
  • Decide whether closed-lid mode should be active.
  • Request privileged helper actions through XPC when needed.
  • Fail closed by restoring normal sleep state on stale or invalid state.

This should start as a user-level LaunchAgent/controller. The menu bar app can later register, observe, or host related setup UX without becoming the only process responsible for power state.

Shared model boundary

AdderailShared contains types that multiple targets need to serialize, display, or send across XPC:

  • Lease
  • LeaseCommand
  • ControllerSnapshot
  • SnapshotStore
  • XPC protocol definitions

Shared data shapes do not imply shared ownership. LeaseManager is intentionally controller-owned because only the controller should mutate live lease state, expire leases, or decide whether power assertions should be active.

A future menu bar app should ask the controller for status and render the returned lease/snapshot data. It may read a persisted snapshot for last-known diagnostics when the controller is unavailable, but that should be treated as stale debug/recovery data.

Lease store

The lease store is a persisted snapshot for recovery and diagnostics, not IPC.

Responsibilities:

  • Record the controller's latest lease snapshot.
  • Provide a readable debugging artifact while the controller-first proof is manual.
  • Support status --json diagnostics.
  • Allow crash/restart recovery.
  • Allow stale-state cleanup.

Rules:

  • The controller is the only writer.
  • CLI commands do not mutate the store.
  • Expired leases must not be restored as active.
  • Store corruption should fail closed and release Adderail-owned awake state.

Likely location:

~/Library/Application Support/Adderail/

Normal awake engine

Normal awake mode should work without admin approval.

Preferred implementation:

  • IOPMAssertion

This prevents normal idle sleep, especially while the lid is open. It is not the complete closed-lid solution.

The assertion is process-owned. If the controller exits or crashes, macOS releases the assertion; the snapshot exists to restore still-valid leases after restart and to aid diagnostics, not to release current IOPMAssertion state.

/usr/bin/caffeinate can be useful for manual experiments, but it should not be the main runtime mechanism once the controller exists.

Privileged helper

The helper is only for root-required closed-lid behavior.

Responsibilities:

  • Run as a root LaunchDaemon.
  • Listen for XPC requests.
  • Validate that callers are signed by the expected app/team.
  • Expose a tiny API.
  • Toggle pmset only through lease-aware methods.
  • Restore stale Adderail-owned state on startup or uninstall.

The helper must not expose a generic "run command" API.

Proposed helper API:

beginClosedLidLease(provider: String, sessionID: String, ttlSeconds: Int)
heartbeatClosedLidLease(sessionID: String, ttlSeconds: Int)
endClosedLidLease(sessionID: String)
status()
restore()

Why Not sudo

sudo runs a command with elevated privileges, usually as root.

That is acceptable for deliberate manual testing, but it is not acceptable in agent hooks or runtime product behavior.

Reasons:

  • Hooks must be non-interactive.
  • sudo may request a password and hang.
  • Runtime password prompts are surprising and unreliable.
  • sudo gives broad command-level privilege instead of a narrow helper API.
  • A signed helper lets the product ask for admin approval once during setup, then perform only the specific privileged operation it was designed for.

Preferred flow:

agent hook/extension -> normal CLI -> user-level controller -> signed privileged helper -> pmset

Avoid:

agent hook/extension -> sudo pmset

AppKit and SwiftUI

AppKit and SwiftUI are both native Apple UI frameworks.

AppKit is the older, mature macOS UI framework. It is imperative and object-oriented. It gives direct access to macOS UI primitives such as windows, menus, status bar items, views, controls, delegates, and event handling.

SwiftUI is newer and declarative. It is closer to React than to Tailwind. You describe UI as a function of state, and SwiftUI updates the rendered UI when state changes.

For this project:

  • SwiftUI is likely enough for the first menu bar UI through MenuBarExtra.
  • AppKit may still be needed for lower-level macOS utility behavior.
  • The two can coexist through hosting APIs.

Recommended Project Shape

Adderail/
  Package.swift
  Sources/
    AdderailCLI/
    AdderailController/
    AdderailShared/
    AdderailApp/        # later
    AdderailHelper/     # later
  memories/
  AGENTS.md
  README.md

Expected products:

  • adderail: command-line bridge called by hooks.
  • adderail-controller: persistent user-level controller / LaunchAgent executable.
  • AdderailApp: later menu bar app and setup surface.
  • AdderailHelper: later privileged helper daemon.
  • AdderailShared: shared lease/status DTOs, snapshot persistence, and XPC protocol definitions. Live lease management stays in AdderailController.

Bundle IDs and Mach service names should be treated as placeholders until the product name and signing identity are chosen.

Things To Learn First

Read enough to understand the surface area before implementation:

  • Swift Package Manager and Package.swift.
  • Swift basics for CLI and macOS process code.
  • NSXPCConnection, NSXPCListener, and NSXPCInterface.
  • launchd, LaunchAgents, Mach services, and launchctl.
  • swift-service-lifecycle for controller process startup and graceful shutdown.
  • IOPMAssertion and pmset -g assertions.
  • Xcode targets, products, schemes, build settings, signing.
  • SwiftUI MenuBarExtra.
  • AppKit status bar and menu concepts.
  • pmset, especially pmset -g, pmset -g assertions, and pmset -g log.
  • ServiceManagement and SMAppService.
  • LaunchDaemons and privileged helper installation.
  • XPC and code-signing requirements across privilege boundaries.
  • Code signing, hardened runtime, and notarization.

Safety Rules

The hard requirement is not visual polish. The hard requirement is that the app never leaves a user's Mac in a surprising power state.

Rules:

  • Treat hooks as best-effort signals, not the source of truth.
  • Make the controller the source of truth for active leases.
  • Prefer restoring normal sleep over staying awake forever.
  • Use TTLs for every lease.
  • Use scheduled expiry timers, not polling, for lease expiration.
  • Treat closed-lid mode as dangerous state.
  • Store the previous system sleep-disabled state before changing it.
  • Restore only settings Adderail changed.
  • Keep an ownership marker for privileged state.
  • Restore on TTL expiry, helper startup, uninstall, app crash recovery, and stale marker detection.
  • Never run arbitrary shell commands from the helper.
  • Never call sudo from hooks.
  • Do not rely on model-chosen tool calls for power-state behavior.
  • Surface failures loudly.

Reference Material

Reference apps:

Apple docs:

Local docs:

man caffeinate
man pmset
man launchctl

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages