English · 简体中文
The green LED on your Caps Lock key as a live status indicator for the Qoder CLI agent.
It blinks while the agent is working, goes solid when the turn is done, and blinks fast when it is waiting on you. It changes nothing about how your keyboard behaves — Caps Lock stays off and your typing stays lowercase.
| State | LED | When |
|---|---|---|
working |
slow blink (0.7s cycle, short flashes) | the agent is thinking / running tools |
waiting |
fast blink (0.22s) | it needs you: permission prompt, question |
done |
solid on | turn finished |
off |
dark | session closed |
A fork of insolowetrust/capslight, which targets Claude Code. Qoder's hook events differ, so the mapping was rebuilt — see Event mapping.
bin/capsled is a small IOKit utility in Swift. It has two ways to light the LED:
hid(default) — writes straight to the keyboard's HID LED element (kHIDPage_LEDs/kHIDUsage_LED_CapsLock). It does not touch the modifier state, so your typing stays lowercase while the light blinks. That's the whole reason not to use the simpler approach below.modifier—IOHIDSetModifierLockState, kept as a fallback. More reliable, but it genuinely turns Caps Lock on, so text would jump to CAPS every time the LED flashes.
To see what your hardware supports: make probe. On a MacBook's built-in keyboard both work,
with no root and no Input Monitoring permission.
bin/caps-indicator is a state machine on top of capsled. It owns the background blink
process and its PID, and tracks state per session in ~/.qoder/capsled/sessions/. With
several CLI windows open, the LED shows the highest-priority state
(waiting > working > done > off) — so one session finishing won't switch off the
indicator for another that's still running.
All you need is the Xcode Command Line Tools (swiftc, python3). No other dependencies.
git clone https://github.com/AyaseEli-Bing/capslight-qoder.git
cd capslight-qoder
./install.shinstall.sh checks prerequisites, builds the binary, probes which backend your hardware
supports, symlinks into ~/.qoder/bin/, and wires up the hooks. Pass --no-test to skip the
visual test at the end.
Your existing hooks in ~/.qoder/settings.json are left alone — this adds its own separate
entries. A backup goes to ~/.qoder/settings.json.bak-capslight before the first change.
Re-running is safe: it replaces its own entries rather than duplicating them.
make install does the same thing without the checks and the test.
The hooks are picked up after you restart your Qoder session.
| Hook | State |
|---|---|
UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, SubagentStart, SubagentStop |
working |
PreToolUse when the tool is AskUserQuestion |
waiting |
PermissionRequest |
waiting |
Stop |
done |
SessionEnd |
off |
Qoder has no Notification event, so nothing has to guess whether a notification was a real
permission prompt or just an idle reminder — PermissionRequest reports that directly. Subagent
and failed-tool events stay on working because the turn is still running through them.
The AskUserQuestion case is read out of the hook payload rather than wired as a separate hook:
in a session that auto-approves tool calls, PermissionRequest never fires, and the agent
waiting on your answer would otherwise look identical to it thinking.
make test # walks through every state with pauses — watch your keyboard
make probe # which backend is available
bin/capsled on # light it manually
bin/capsled off
bin/caps-indicator reset # panic button: kill the blinker, turn the LED offIf nothing happens, start with make probe. If both backends come back unavailable, that
keyboard won't hand over its LED (happens on some third-party Bluetooth models).
If the LED lights up but shows the wrong thing, turn on the breadcrumb log and watch what the hooks actually send:
touch ~/.qoder/capsled/debug # start logging
tail -f ~/.qoder/capsled/debug.log # one line per hook: argument, event, payload
rm ~/.qoder/capsled/debug # stop (the log file stays until you delete it)CAPSLIGHT_DEBUG=1 does the same for a single run.
By default every HID keyboard on the machine is driven, external ones included. Limit that with a persisted switch — it survives restarts and applies to all sessions at once:
caps-indicator scope internal # only the built-in keyboard
caps-indicator scope external # only external keyboards
caps-indicator scope all # every keyboard (default)
caps-indicator status # show the current scopeTo override for one run instead, set CAPS_KEYBOARD=internal|external|all in the environment the
CLI is launched from.
A keyboard counts as built-in when its IOKit Transport is FIFO, which is how Apple Silicon
reports the internal keyboard/trackpad; anything else (USB, Bluetooth) counts as external. Check
what your machine has, and whether a device can be lit at all:
bin/capsled list
use transport=FIFO product=Apple Internal Keyboard / Trackpad capsLockLEDs=1
use transport=USB product=USB Receiver capsLockLEDs=1
use transport=USB product=G502 HERO Gaming Mouse capsLockLEDs=0
use transport=Virtual product=TouchBarUserDevice capsLockLEDs=0capsLockLEDs=0 means the device exposes no Caps Lock LED element, so writes to it change
nothing. Scope applies only to the hid backend: the modifier fallback goes through
IOHIDSystem and cannot target one device.
Via environment variables (set them in ~/.qoder/settings.json under env):
CAPS_WORKING_INTERVAL=0.7 CAPS_WORKING_DUTY=0.35
CAPS_WAITING_INTERVAL=0.22 CAPS_WAITING_DUTY=0.5
CAPS_IDLE_SECONDS=90 CAPS_BLINK_MAX_SECONDS=900 CAPS_SESSION_TTL_MINUTES=10
DUTY is the fraction of each cycle the LED stays lit. 0.35 gives short flashes (less
distracting in peripheral vision), 0.5 gives an even blink.
To force a backend: bin/capsled --backend modifier on.
Sometimes you don't want a blinking keyboard — a screen recording, a meeting, a demo. No need to uninstall:
caps-indicator disable # LED goes dark and stays dark
caps-indicator enable # back to normal on the next hook
caps-indicator status # muted or active, and the current stateThe hooks stay wired; they just return immediately and do nothing. The switch is a flag file at
~/.qoder/capsled/disabled, so it survives restarts and applies to every session at once.
make disable / make enable / make status do the same from the checkout.
To mute a single session instead of all of them, launch the CLI with CAPSLIGHT_DISABLED=1 in
its environment.
caps-indicator reset keeps working while muted — it's the way out if the LED ever gets stuck.
make uninstallTurns the LED off, removes the hooks from ~/.qoder/settings.json, deletes the symlinks and
clears the state directory. Your original settings are still in
~/.qoder/settings.json.bak-capslight.
Worth knowing before you run a script that edits your config:
~/.qoder/settings.json— adds one command hook per event listed above, nothing else. A pristine copy is kept as.bak-capslightbefore the first change, and writes go through a temp file plus atomic rename, so an interrupted run can't truncate your settings. If the file isn't valid JSON, the installer says so and changes nothing.~/.qoder/bin/— two symlinks pointing back at this checkout.~/.qoder/capsled/— the blinker's PID, per-session state and the mute flag.
Nothing runs as root, nothing is downloaded, nothing phones home.
- Pressing the real Caps Lock key mid-blink makes the system overwrite the LED state. The next flash puts it back, but you may see one beat go out of step.
- There is only one LED for the whole machine, so a session that ends abruptly can pin it. Three
causes, three reclamation rules, all run on the next hook from any session:
- The host process died → the pid stored with each record fails
kill -0, record dropped. - A turn was stopped with the stop button → it emits neither
StopnorSessionEndwhile the app stays alive. Its transcript then goes quiet, so a record whose transcript has not changed forCAPS_IDLE_SECONDS(90s) and whose last conversation entry is still the user's own prompt is dropped. - Owner pid unresolvable → the record ages out after
CAPS_SESSION_TTL_MINUTES(10 min). One hole is left: stopping during a long tool call leaves an assistant entry as the last record, so only the blinker's ownCAPS_BLINK_MAX_SECONDS(15 min) clears it. Runbin/caps-indicator resetto clear it immediately.
- The host process died → the pid stored with each record fails
- External keyboards are driven too — the command goes to every HID keyboard at once.
Upstream author: Max Solo — insolowetrust/capslight.
MIT. Do whatever you like with it.