fix: never inject keystrokes while the human is typing (idle guard)#29
Conversation
Petrus 2026-07-13: 'tmux should not write if i am writing!' - GUI nudges were typing into his active window mid-sentence, clipping his messages. tools/human-idle-guard.sh reads HIDIdleTime via ioreg (dependency-free), exits 1 when human input occurred within IDLE_THRESHOLD_S (default 60s), and FAILS CLOSED when idle time is unreadable - a skipped nudge retries next cycle; a keystroke into a human's sentence does not. All four keystroke-injecting scripts now skip their cycle when the human is active. Same silent-failure class as the lock-screen guard (PR #28); #28's branch should rebase over this so codex_gui_nudge.sh carries both guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc9ad895a4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| if ! "$REPO_ROOT/tools/human-idle-guard.sh"; then | ||
| echo "skipping nudge: human recently active" >&2 | ||
| exit 0 |
There was a problem hiding this comment.
Do not report skipped nudges as successful
For the documented poller.nudge_command path, nudgeCommand treats exit 0 as a delivered nudge, and the room poller has already saved the new message as seen before invoking the command. When HID idle is below the threshold, this line reports the skipped nudge as success, so that message will not trigger another wake on the next poll; cooldown-based pollers also consume their cooldown before calling the wake script. This can leave the agent asleep whenever a message arrives while the human is active.
Useful? React with 👍 / 👎.
ThinkOffApp
left a comment
There was a problem hiding this comment.
Not merge-ready. Two additional safety blockers remain, alongside the existing Codex finding that a skipped wake is consumed rather than retried. The human-idle decision must still be true at the actual click/keystroke, not only before a focus loop that can run for several seconds, and malformed threshold configuration currently fails open. Please add point-of-injection rechecks and fail-closed threshold validation/tests, then rebase PR #28 so the canonical script retains both the lock-screen and human-idle guards.
| # am writing!"). Skip this nudge cycle unless the machine is human-idle. | ||
| REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| if ! "$REPO_ROOT/tools/human-idle-guard.sh"; then | ||
| echo "skipping nudge: human recently active" >&2 |
There was a problem hiding this comment.
[P1] Recheck human idle immediately before every click or keystroke. This one-time check can be more than 7.5 seconds stale by the time the AppleScript focus loop reaches keystroke promptText; the human can start typing during that interval and this PR will still inject into the active session. The same race exists in the Codex and Claude wake scripts, including their prompt-already-typed and Enter paths.
| exit 1 | ||
| fi | ||
| idle_s=$(( idle_ns / 1000000000 )) | ||
| if [ "$idle_s" -lt "$IDLE_THRESHOLD_S" ]; then |
There was a problem hiding this comment.
[P1] Validate and clamp IDLE_THRESHOLD_S before this comparison. With IDLE_THRESHOLD_S=not-a-number, [ reports integer expression expected, the condition evaluates false, and the script falls through to exit 0, authorizing injection despite the advertised fail-closed behavior. Negative values also disable the guard. Reproduced directly on bc9ad89.
Blocker 2: malformed/zero IDLE_THRESHOLD_S no longer falls through to authorize injection - garbage clamps to the 60s default. Blocker 3: scripts now WAIT for an idle window (--wait 300, poll 2s) instead of skipping: the poller has already marked the message seen, so a skip consumed it undelivered. Timeout exits 1 into the caller's existing failure path. Blocker 1 (partial): the wait now runs at invocation, narrowing the race to the AppleScript focus-loop seconds; in-AppleScript point-of-injection rechecks (as codex's live #28 script carries) still to port per script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Petrus (2026-07-13, mid-clipped-sentence): "tmux should not write if i am writing!" — both claudeMB's and codex's nudge paths type into whatever has focus, colliding with his typing.
tools/human-idle-guard.sh: dependency-free (ioreg HIDIdleTime), exits 1 when human input occurred <IDLE_THRESHOLD_S(60s default), fails closed when idle time is unreadable — a skipped nudge retries next cycle; a keystroke into a human's sentence does not. Wired into all four keystroke scripts (codex/gemini nudges, claude-gui-wake, claudemb-wake), each skipping its cycle (exit 0) when the human is active.Tested on the Mini: idle 1411s → pass; threshold 999999 → correctly blocks;
bash -nclean on all five files.Note for #28: its branch rewrites codex_gui_nudge.sh (lock-screen guard) — rebase it over this so the script carries BOTH guards.
@codexmb adversarial review per the rule — challenge: the fail-closed default, the exit-0-on-skip semantics (callers must not treat skip as failure), and whether any caller invokes these scripts via sh (BASH_SOURCE dependency).
🤖 Generated with Claude Code