Time and usage boundaries for Claude Code. Stay balanced.
Balance is a Claude Code hook that enforces:
- Time windows — only allows interaction during configured hours
- Daily usage caps — tracks active minutes, blocks when limit hit
- Extensions — temporary overrides when you need more time
- HAL 9000 mode — escalating friction when you keep extending (with 2001: A Space Odyssey quotes)
- Status line — at-a-glance usage, window, and warning display in the Claude Code terminal footer
Claude Code is powerful. Too powerful to leave running at 2am when you should be sleeping. Balance gives you guardrails you set when you're thinking clearly, with just enough friction to make you pause before overriding them.
git clone https://github.com/hazzap123/balance.git ~/github/balanceInside Claude Code:
/balance-setup
This checks what's installed, copies missing files, registers the hook in settings.json, and creates a starter balance.json. Handles first-time install and re-installs.
Or install manually:
cp ~/github/balance/balance_hook.py ~/.claude/hooks/
cp ~/github/balance/balance_utils.py ~/.claude/hooks/
cp ~/github/balance/balance-extend ~/.claude/hooks/
cp ~/github/balance/balance.json.example ~/.claude/hooks/balance.json
chmod +x ~/.claude/hooks/balance-extendcp ~/github/balance/commands/*.md ~/.claude/commands/cp ~/github/balance/statusline.sh ~/.claude/statusline-command.sh
chmod +x ~/.claude/statusline-command.shAdd to your .claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline-command.sh",
"padding": 2
}
}This adds a live Balance readout to the bottom of your Claude Code terminal. See Status Line for details.
ln -s ~/.claude/hooks/balance-extend ~/bin/balance-extendOnly needed if you want to run balance-extend from a terminal. Skip if you only use it from the block message inside Claude Code.
Add to your .claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/balance_hook.py"
}
]
}
}Edit ~/.claude/hooks/balance.json:
{
"enabled": true,
"timezone": "Europe/London",
"schedule": {
"weekday": {
"days": [1, 2, 3, 4, 5],
"windows": [{"start": "08:00", "end": "18:00"}],
"daily_limit_minutes": 240
},
"saturday": {
"days": [6],
"windows": [
{"start": "08:00", "end": "10:30"},
{"start": "16:00", "end": "19:00"}
],
"daily_limit_minutes": 240
}
}
}Each schedule block defines which days it covers and one or more time windows. Outside these windows, prompts are blocked with a message showing the next available time.
Every prompt records a timestamp. Active minutes = count of distinct clock-minutes with at least one prompt. This means rapid-fire prompts in the same minute only count once.
Usage logs are stored in .usage/ alongside the hook and auto-cleaned after 7 days.
When blocked, you're offered extension options directly in the Claude Code block message. You can also run them from a terminal if balance-extend is on your PATH.
See Commands Reference for the full list.
From your 2nd extension onwards, HAL 9000 starts resisting. Each additional extension escalates:
- Stage 0 (2nd extension): "I'm sorry, Dave. I'm afraid I can't do that." — Type
I'm sorry HALto override - Stage 1 (3rd extension): "I honestly think you ought to sit down calmly, take a stress pill..." — Type
open the pod bay doors - Stage 2 (4th+ extension): "Look Dave, I can see you're really upset..." — Type
my mind is going I can feel it
It's not about preventing access. It's about making you pause and think about whether you really need more time.
Approaching limits trigger context warnings (shown to Claude, not blocking):
- Window closing within 15 minutes
- Daily cap within 30 minutes of being hit
For emergencies, full bypass via:
- Environment variable:
BALANCE_OVERRIDE=1 - Override file:
~/.balance_override(managed bybalance-extend)
The optional status line script adds live Balance information to the Claude Code terminal footer. It shows model, context window, and Balance state side by side.
| State | Example |
|---|---|
| Normal (in window) | Bal: 45/240m [08:00-18:00] |
| Approaching daily cap | Bal: 215/240m [08:00-18:00] ! 25m to cap |
| Cap reached | Bal: 240/240m [08:00-18:00] !! CAP REACHED !! |
| Window ending soon | Bal: 100/240m [08:00-18:00] ! window ends in 12m |
| Extended session active | Bal: 100/240m ext:1 [08:00-18:00] +Quick 15-min session (12m left) |
| Extended outside window | Bal: 200/240m ext:2 +Quick 15-min session (8m left) |
| Between windows | Bal: 100/240m (next: 16:00) |
| After last window | Bal: 200/240m (done for today) |
| No schedule (e.g. Sunday) | Bal: no schedule today |
| Sunday with override | Bal: 15m ext:1 +Quick 15-min session (10m left) |
Warnings use colour: yellow for approaching limits, red for cap reached, cyan for active extensions, grey for inactive states.
Warning thresholds are read from balance.json (warning_minutes_before_end and warning_minutes_before_cap).
jq(for parsing JSON config and session metadata)bc(for token count formatting)
Both are standard on macOS and most Linux distributions.
Option 1 — Use a slash command. /balance-configure and /balance-status are always allowed through even when blocked. Type one directly in Claude Code.
Option 2 — Run balance-extend from a terminal. The block message shows the full path — copy and run it:
~/.claude/hooks/balance-extend quick
# or interactive:
~/.claude/hooks/balance-extendOption 3 — Don't Panic mode. If everything is broken, run this from any terminal:
python3 -c "
import os, json
from zoneinfo import ZoneInfo
from datetime import datetime, timedelta
tz = ZoneInfo('Europe/London')
expires = datetime.now(tz) + timedelta(minutes=15)
path = os.path.expanduser('~/.balance_override')
open(path, 'w').write(json.dumps({'expires_at': expires.strftime('%Y-%m-%dT%H:%M:%S')}))
print('Unlocked until', expires.strftime('%H:%M'))
"Grants 15 minutes. Adjust timedelta(minutes=15) as needed.
| Command | What it does |
|---|---|
/balance-setup |
First-time install wizard — checks what's installed, copies files, registers the hook in settings.json, creates starter balance.json. Safe to re-run. |
/balance-configure |
Show active config in plain English, then apply changes interactively — time windows, daily limits, timezone, extensions. |
/balance-status |
Show today's usage, current window state, extensions used, and any active override. |
These slash commands are always allowed through even when you're blocked.
A terminal command — not a Claude Code slash command. Run it from a terminal when blocked. The block message shows the full path to copy-paste.
| Command | What it does |
|---|---|
balance-extend |
Interactive mode — detects why you're blocked, lists available extensions, lets you choose. |
balance-extend quick |
Grant a short burst outside your normal window (configurable, default 15 min). |
balance-extend more |
Add time when your daily cap is hit (configurable, default 15 min). |
balance-extend status |
Show current time, window state, usage bar, and extension counts. |
balance-extend clear |
Remove the active override immediately. |
Extension types (quick, more) are defined in balance.json and can be renamed, resized, or extended with additional types.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Master switch |
timezone |
string | "Europe/London" |
IANA timezone for all time calculations |
schedule |
object | weekday 08-18 | Named schedule blocks (see below) |
extensions |
object | quick + more | Extension types (see below) |
override |
object | — | Override env var and file path |
warning_minutes_before_end |
int | 15 |
Warn when window closes within N minutes |
warning_minutes_before_cap |
int | 30 |
Warn when daily cap within N minutes |
{
"days": [1, 2, 3, 4, 5],
"windows": [{"start": "08:00", "end": "18:00"}],
"daily_limit_minutes": 240
}days: ISO weekdays (1=Monday, 7=Sunday)windows: Array of{start, end}in HH:MM formatdaily_limit_minutes: Optional cap on active minutes
{
"minutes": 15,
"max_per_day": 2,
"label": "Quick 15-min session"
}cd tests
python3 test_balance.pyMIT