Keep your Claude Code & Codex usage windows lit — on a schedule you choose.
A tiny macOS launchd scheduler that sends low-cost Claude Code and Codex check-ins at
fixed times, then records activation logs, per-run token usage, and quota status snapshots.
Website · Features · Quick Start · Menu Bar App · How It Works · Cost · Configuration · Download
Activity · Light![]() |
Activity · Dark![]() |
Stoker is a small Bash-based utility for people who want predictable Claude Code and Codex
usage-window start times. It installs a macOS launchd agent that runs in a dedicated
lightweight folder, asks each CLI to reply READY, and keeps the prompt intentionally small
so it does not scan real projects or modify files.
The name is a nod to a stoker — the crew member who keeps a furnace fed so the fire never goes out. That is exactly what this tool does for your AI usage windows: it keeps them lit on a schedule you choose.
The default schedule is 07:00, 12:00, 17:00, and 22:00 local macOS time.
| Feature | |
|---|---|
| ⏰ | Scheduled activation of Claude Code and Codex through macOS launchd. |
| 🪶 | A minimal prompt that tells both CLIs not to inspect files, run tools, or modify anything. |
| 📜 | Human-readable run history in logs/activation.log. |
| 📊 | Structured per-run usage records in logs/usage.jsonl. |
| 🔋 | Five-hour and weekly quota snapshots in logs/status.jsonl. |
| 🚦 | Quota preflight that skips activation gracefully when a known quota is exhausted. |
| 🧬 | Clone-friendly configuration through .env. |
| 🛟 | Safe manual commands for dry runs, dependency checks, quota checks, and uninstall. |
- macOS with
launchctl. - Bash.
- Claude Code CLI, authenticated with your Claude plan.
- Codex CLI, authenticated with ChatGPT.
jqfor structured JSONL parsing.- Node.js for Codex quota status queries.
omc/ oh-my-claudecode for Claude quota status snapshots.
The activation itself only requires the Claude and Codex CLIs. Quota snapshots gracefully warn
and skip if optional helpers such as omc, node, or jq are missing.
Choose one distribution:
- CLI/launchd package: for advanced users who want the lightest possible install and direct shell control.
- Menu bar app package: for beginners who want a GUI monitor and settings panel while keeping the same local scheduler underneath.
git clone https://github.com/hakupao/stoker.git
cd stoker
cp .env.example .env
./install.sh check
./install.sh dry-run
./install.sh./install.sh defaults to install, which generates a user LaunchAgent and loads it into the
current macOS GUI session.
Download the GUI DMG, drag Stoker.app to Applications, open it, then use the status-bar
menu to install/reload the schedule, refresh quota, run once, pause the schedule, and edit
settings. The app bundles the same CLI engine and installs its working copy under
~/Library/Application Support/Stoker/stoker.
See INSTALL.md for complete beginner and advanced installation steps.
./install.sh check # verify local dependencies
./install.sh dry-run # show commands without sending model prompts
./install.sh quota # query quota status without sending model prompts
./install.sh app-status # print JSON status for the menu bar app
./install.sh status # show launchd status
./install.sh run-now # trigger once; this sends model prompts
./install.sh uninstall # unload and remove the LaunchAgent
./install.sh print-plist # print the generated launchd plistYou can also call the runner directly:
./bin/activate-ai-window.sh --once
./bin/activate-ai-window.sh --status
./bin/activate-ai-window.sh --once --tool claude
./bin/activate-ai-window.sh --once --tool codexUse these checks to confirm the timer is installed, waiting, and recording results:
./install.sh status
tail -f logs/activation.log
tail -n 20 logs/usage.jsonl | jq
tail -n 20 logs/status.jsonl | jq./install.sh status should show a loaded LaunchAgent with calendar triggers for your
configured hours. state = not running is normal between scheduled runs; it means the job is
loaded and waiting for the next trigger. During a trigger it may briefly show running.
logs/activation.log is the quickest human-readable view. A normal run looks like this:
Activation run started ...
Quota preflight started
Claude job started
Codex job started
Activation run finished exit=0
If quota preflight decides not to send a prompt, the run stays clean and records a skip:
claude job skipped by quota preflight reason=quota_exhausted
codex job skipped by quota preflight reason=quota_exhausted
logs/usage.jsonl is the structured success/skip record. Successful activations usually
include ok: true, result: READY, and exit_code: 0. Skipped activations include
skipped: true and a skip_reason.
Manual command guide:
./install.sh status: checks whether locallaunchdhas the timer loaded../install.sh quota: checks quota status without sending prompts../install.sh dry-run: prints the planned commands without sending prompts../install.sh run-now: triggers the installed LaunchAgent once and may consume usage if quota is available.
Copy .env.example to .env and adjust values:
| Variable | Description | Default |
|---|---|---|
LABEL |
macOS LaunchAgent label | com.stoker.ai-window |
SCHEDULE_TIMES |
Comma-separated HH:MM schedule entries; each time point is independent |
"07:00,12:00,17:00,22:00" |
ACTIVATION_TOOL |
all, claude, or codex |
all |
ACTIVATION_PROMPT |
Low-cost prompt sent to the CLIs | Reply exactly READY... |
CODEX_MODEL |
Codex activation model; set default to let Codex CLI choose |
gpt-5.4-mini |
TIMEOUT_SECONDS |
Per-tool timeout | 120 |
ENABLE_STATUS_SNAPSHOTS |
Record quota snapshots after real activation | 1 |
ENABLE_QUOTA_PREFLIGHT |
Check quota before sending prompts | 1 |
QUOTA_PREFLIGHT_ON_UNKNOWN |
allow or skip when quota cannot be checked |
allow |
QUOTA_EXHAUSTED_THRESHOLD_PERCENT |
Skip when remaining quota is at or below this percent | 0 |
KEEP_AWAKE_MODE |
off, during, or always; scheduled CLI runs use caffeinate when not off |
off |
KEEP_AWAKE_SECONDS |
Bounded keep-awake duration for each real activation run | 900 |
CLAUDE_BIN |
Optional Claude binary override | auto-discovered |
CODEX_BIN |
Optional Codex binary override | auto-discovered |
JQ_BIN |
Optional jq binary override |
auto-discovered |
NODE_BIN |
Optional Node.js binary override | auto-discovered |
OMC_BIN |
Optional omc binary override |
auto-discovered |
PATH_VALUE |
PATH used by launchd and the runner | Homebrew/local/system defaults |
After changing schedule or label values, reinstall the LaunchAgent:
./install.sh installtail -f logs/activation.log
tail -20 logs/usage.jsonl | jq
tail -20 logs/status.jsonl | jqLog files:
logs/activation.log: human-readable run history.logs/usage.jsonl: one structured usage snapshot per tool per real run.logs/status.jsonl: five-hour and weekly quota snapshots per tool.logs/raw/: raw Claude/Codex/status outputs for debugging and future parsing.logs/launchd.out.logandlogs/launchd.err.log: launchd stdout/stderr.
The CLI/launchd workflow remains the primary engine. The optional menu bar app is a separate beginner-friendly distribution that adds a macOS status-bar control surface for the same configuration, schedule, quota snapshots, and logs. The interface follows the system Light/Dark appearance and warms from a cool idle palette to a warm ember "active" state when the schedule is on.
Highlights:
- Activity dashboard — per-tool quota-trend chart (5-hour / weekly), a run-history timeline with expandable per-run details (tokens, cost, duration, session), and summary stats with date-range / status / tool filters.
- Settings — edit independent schedule times, toggle Claude/Codex, and configure advanced options (quota preflight, post-run snapshots, keep-awake, launch at login).
- Bilingual UI with an EN / 中 switch; the appearance follows the system Light/Dark setting.
- Environment Check that detects required and optional CLI tools.
- Export run history to CSV.
Build the app bundle locally:
./app/StokerMenuBar/build-app.sh
open "dist/Stoker.app"The app calls the existing scripts instead of replacing them:
./install.sh app-statusfor a JSON state snapshot../install.sh installto save/reload the LaunchAgent after settings changes../install.sh run-now,quota,dry-run, anduninstallfor menu actions.
Set KEEP_AWAKE_MODE=always in the app if you want it to keep macOS awake while the menu bar
app is open. Scheduled activation still works without the app running, and
KEEP_AWAKE_MODE=during protects only real activation runs.
Maintainers can build both publishable artifacts with one command:
./scripts/package-release.shThe output under dist/ is split by audience:
stoker-cli-<version>.tar.gz: lightweight CLI/launchd package.stoker-gui-<version>.dmg: GUI app package for beginner users.stoker-gui-<version>.zip: fallback GUI app archive.
The project has two entry points — CLI and menu bar app — that share the same shell engine:
flowchart TB
subgraph entry["Entry points"]
L["launchd<br/><i>(scheduled at HH:MM)</i>"]
A["Menu bar app<br/><i>(SwiftUI GUI)</i>"]
end
subgraph engine["Shared shell engine"]
R["bin/activate-ai-window.sh<br/><i>activation runner</i>"]
S["bin/activation-state.sh<br/><i>JSON state query</i>"]
I["scripts/install-launchd.sh<br/><i>launchd manager</i>"]
end
C["Claude Code CLI"]
X["Codex CLI"]
L -- "triggers" --> R
A -- "Process()" --> R
A -- "Process()" --> S
A -- "Process()" --> I
R -- "minimal prompt" --> C
R -- "minimal prompt" --> X
When launchd triggers at a scheduled time (or you run ./install.sh run-now), the activation
script executes this sequence:
- Load config — reads
.envfor schedule, tool selection, quota settings, and binary paths. - Acquire lock — creates
run/activation.lockto prevent concurrent runs; a second trigger during an active run is skipped gracefully. - Quota preflight (optional) — queries Claude and Codex quota status before sending any
prompt. If a tool's quota is exhausted, that tool is skipped and the skip is recorded in
logs/usage.jsonl. - Send prompt — calls each enabled CLI with a minimal prompt (
Reply exactly READY). Claude runs in ultra-lightweight mode (see Cost Optimization). Codex runs on the configured lightweight model with--ephemeral,--skip-git-repo-check,--sandbox read-only, and stripped-down config (see below). - Record usage — parses each CLI's JSON output with
jqand appends a structured record tologs/usage.jsonl(token counts, cost, session ID, model, duration, etc.). - Post-run snapshots (optional) — takes another quota snapshot after activation and
appends it to
logs/status.jsonl. - Release lock — removes the lock directory so the next scheduled run can proceed.
Timeout protection: each CLI call is wrapped in a background process with a configurable timeout (default 120 s). If a CLI hangs, it receives SIGTERM, then SIGKILL after 2 s.
The SwiftUI app is a thin GUI shell — it does not contain its own scheduler or activation logic. Every operation delegates to the same shell scripts:
| App action | Shell call |
|---|---|
| Read status | bin/activation-state.sh --json |
| Toggle schedule | install.sh install or install.sh uninstall |
| Save settings | Write .env, then install.sh install |
| Run once | install.sh run-now |
The app calls scripts through Process() (Foundation), reads stdout, and decodes the JSON into
Swift model types.
Both CLIs are invoked inside the stoker project directory itself — never inside your real projects. This is a lightweight folder that contains only scripts and logs, so there is nothing for the CLIs to scan or modify.
| Installation method | Working directory | Who creates it |
|---|---|---|
CLI (git clone) |
The cloned repo, e.g. ~/stoker |
You, when you clone |
| Menu bar app (dev build) | Same cloned repo | Same |
| Menu bar app (.app / DMG) | ~/Library/Application Support/Stoker/stoker/ |
App creates it automatically on first launch by copying scripts from the app bundle |
How the directory is resolved:
- Shell scripts:
ROOT_DIRis computed at runtime by walking up from the script's own location (bin/) to find the parent directory. This means the project works from any clone path without editing scripts. - Menu bar app:
ProjectLocatorwalks up from the app bundle to find a directory containingbin/activate-ai-window.sh. For a standalone.app, it falls back to copying bundled scripts into Application Support and using that copy as the root.
The installer writes an absolute-path plist for macOS launchd and places it under
~/Library/LaunchAgents/. The plist is intentionally git-ignored because it contains
machine-specific paths.
stoker/
├── bin/
│ ├── activate-ai-window.sh ← activation runner
│ └── activation-state.sh ← JSON state for the app
├── scripts/
│ └── install-launchd.sh ← launchd install/uninstall
├── app/
│ └── StokerMenuBar/ ← SwiftUI menu bar app
├── launchd/ ← generated plist (git-ignored)
├── logs/ ← generated logs
│ ├── activation.log
│ ├── usage.jsonl
│ ├── status.jsonl
│ └── raw/
├── .env.example
├── install.sh ← user-facing entry point
└── README.md
GitHub Actions only validates the repository scripts on push and pull requests. Scheduled
activation always runs locally on the Mac where ./install.sh install was executed.
Each activation only needs a single API round-trip — the prompt and response together are under 300 tokens. The cost challenge is the system prompt that each CLI injects automatically (CLAUDE.md, plugins, MCP tool descriptions, hooks, etc.), which can exceed 40 000 tokens per call.
The runner strips both CLIs down to the absolute minimum context required:
| Flag | Effect |
|---|---|
--model haiku |
Cheapest model (input ~$0.80/M vs Opus ~$15/M) |
--system-prompt "Reply only: READY" |
Custom minimal system prompt |
--setting-sources "" |
Skip loading CLAUDE.md, hooks, and plugin instructions — eliminates ~40K tokens of injected context |
--effort low |
Minimal reasoning effort |
--strict-mcp-config --mcp-config '{"mcpServers":{}}' |
Empty MCP config — removes all tool descriptions |
--tools "" |
Disable all built-in tools |
--disable-slash-commands |
Disable skills |
Result: ~170 input tokens, ~$0.001 per activation (vs ~40K tokens / ~$0.15 without optimization).
| Flag | Effect |
|---|---|
--ignore-user-config |
Skip ~/.codex/config.toml — removes plugins, MCP servers, developer instructions |
--ignore-rules |
Skip .rules files |
--model "$CODEX_MODEL" |
Use the configured lightweight activation model (gpt-5.4-mini by default) |
-c 'features.memories=false' |
Disable memories |
-c 'features.multi_agent=false' |
Disable multi-agent |
-c 'features.goals=false' |
Disable goals |
-c 'features.codex_hooks=false' |
Disable hooks |
-c 'features.child_agents_md=false' |
Disable AGENTS.md loading |
-c 'model_reasoning_effort="low"' |
Minimal reasoning effort |
Result: ~22K input tokens (vs ~32K without optimization). Codex's internal system prompt
(~22K) is still the token floor, but gpt-5.4-mini spends the lighter local-message allowance
for routine activation turns. Set CODEX_MODEL=default if you prefer the Codex CLI default.
| Tool | Before | After |
|---|---|---|
| Claude | ~$18/month | ~$0.16/month |
| Codex | Quota-based, ~32K tokens/call | Quota-based, ~22K tokens/call (−31%) |
dry-rundoes not send model prompts.quotaonly queries account/rate-limit status paths and local caches; it does not send a model prompt.run-nowand scheduled activation first run quota preflight, then send one small prompt per enabled tool only when quota appears available.- If quota is known to be exhausted, the tool is skipped and recorded in
logs/usage.jsonlwithskipped: true. - The Claude invocation uses
--model haiku,--setting-sources "",--system-prompt,--effort low,--strict-mcp-configwith an empty config, no tools, no slash commands, and no session persistence. See Cost Optimization for details. - The Codex invocation uses
--model "$CODEX_MODEL",--ephemeral,--skip-git-repo-check,--sandbox read-only,--ignore-user-config,--ignore-rules, and disables features like memories, multi-agent, goals, and hooks. - The generated plist is intentionally ignored by git because it contains machine-specific absolute paths.
./install.sh uninstallIf you are migrating from an older local label, set LEGACY_LABELS="old.label" when installing
so the old LaunchAgent is removed and does not double-trigger.
Run local validation before sending changes:
./scripts/validate.shSee CONTRIBUTING.md for development notes.
See CHANGELOG.md for release history.
Stoker is an independent, open-source project. It is not affiliated with, endorsed by, or sponsored by Anthropic, OpenAI, or Apple. "Claude", "Claude Code", and "Anthropic" are trademarks of Anthropic; "Codex", "ChatGPT", and "OpenAI" are trademarks of OpenAI; "macOS" and "Apple" are trademarks of Apple Inc. — used here only to identify the tools Stoker works with.
Stoker sends automated check-in prompts to the Claude Code and Codex CLIs on your behalf and consumes real usage/quota. You are solely responsible for ensuring your use complies with the applicable Terms of Service, usage policies, and rate limits of Anthropic and OpenAI, and for any resulting costs or account actions. The software is provided "as is", without warranty of any kind. Stoker runs entirely on your Mac and sends no data to the project authors.
See DISCLAIMER.md for the full disclaimer and trademark, privacy, and third-party notices, and SECURITY.md to report a vulnerability.
Distributed under the MIT License. See LICENSE for more information.



