Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions scripts/claude-hooks/tests/test_guard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,93 @@ def test_add_all_and_reset_hard_now_agree_about_the_global_option_hop():
assert gc.evaluate(cmd, "claude-code") is not None, cmd


# --------------------------------------------------------------------------- #
# 1c. 🔴 `git add <specific-files>` stays allowed
#
# Closes a false positive from the opencode.jsonc deny glob `"*git*add ."` which
# matched `git add clusters/foo.yaml` (glob `*` consumed the path, `.` matched
# the dot in `foo.yaml`). The guard_core.py argv parser is correct —
# _stages_everything() properly tokenizes arguments and only flags `-A`, `--all`,
# `.`, `..`, `*`, `$PWD` — but the glob backstop over-blocked.
#
# After the fix (removing `"*git*add ."` from opencode.jsonc), these cases
# exercise guard_core.py directly to pin that specific-file staging stays allowed.
# --------------------------------------------------------------------------- #
ADD_ALL_DENY = [
"git add -A",
"git add --all",
"git add .",
"git add ..",
"git add *",
"git add -Av",
"git add -A -- foo",
"git -C /tmp/x add -A",
"VAR=1 git add .",
"sudo git add .",
"sudo -n git -C /tmp/x add -A",
"env git add .",
"timeout 60 git add .",
"bash -c 'git add .'",
"echo ok && git add .",
"cd /tmp && git add .",
]

ADD_ALL_ALLOW = [
"git add clusters/foo.yaml",
"git add src/main.py",
"git add -p",
"git add --interactive",
"git add clusters/workbench/apps/media-stack/kustomization.yaml",
"git -C /tmp/x add src/main.py",
"VAR=1 git add src/main.py",
"sudo git add src/main.py",
"git status",
"git diff",
]


@pytest.mark.parametrize("command", ADD_ALL_DENY)
def test_git_add_all_denies_all_staging_spelling(command):
"""DENY cases — commands that stage the whole tree must be caught.

These exercise check_git_add_all directly and via evaluate() under both
policies (the check is in both claude-code and opencode).
"""
assert gc.check_git_add_all(command) is not None, f"check_git_add_all missed: {command!r}"
assert gc.evaluate(command, "claude-code") is not None, f"evaluate missed (claude-code): {command!r}"
assert gc.evaluate(command, "opencode") is not None, f"evaluate missed (opencode): {command!r}"


@pytest.mark.parametrize("command", ADD_ALL_ALLOW)
def test_git_add_specific_files_stays_allowed(command):
"""ALLOW cases — staging specific files must not be blocked.

This is the false positive that motivated removing `"*git*add ."` from
opencode.jsonc. The argv parser in _stages_everything() correctly
distinguishes `-A`/`.` from specific file paths; pin it here so a
regression cannot re-introduce the glob's over-matching.
"""
assert gc.check_git_add_all(command) is None, f"check_git_add_all false-positived: {command!r}"
assert gc.evaluate(command, "claude-code") is None, f"evaluate false-positived (claude-code): {command!r}"
assert gc.evaluate(command, "opencode") is None, f"evaluate false-positived (opencode): {command!r}"


def test_git_add_escape_hatch_message_stays_denied():
"""The escape hatch: merely QUOTING `git add .` in a message still denies.

check_git_add_all matches raw text, so a commit message mentioning the
blocked shape is caught — the same deliberate false-positive convention
the other checks follow. The deny message includes the escape hatch
(Write tool + commit -F / --body-file).
"""
cmd = 'git commit -m "never use git add ."'
reason = gc.check_git_add_all(cmd)
assert reason is not None, "quoted mention of git add . must still be caught"
assert "commit -F" in reason
assert "--body-file" in reason
assert "Write tool" in reason


# --------------------------------------------------------------------------- #
# 2. the parser
# --------------------------------------------------------------------------- #
Expand Down
50 changes: 37 additions & 13 deletions scripts/monitor-blackout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ set -euo pipefail
DDC=$(command -v ddcutil) || { echo "ddcutil not found on PATH" >&2; exit 1; }
UNIT=monitor-blackout-restore
STATE="${XDG_RUNTIME_DIR:-/tmp}/monitor-blackout.state" # "bus:brightness"
FADE_PID_FILE="${XDG_RUNTIME_DIR:-/tmp}/monitor-blackout.fade.pid"

# Number of fade steps and inter-step delay for fade transitions.
# ~560ms per ddcutil call (hard floor at --sleep-multiplier 0.1), so 15 steps
Expand All @@ -44,8 +45,14 @@ get_brightness() { # $1=bus -> current VCP 0x10 value
"$DDC" --bus "$1" getvcp 10 --brief 2>/dev/null | awk '{print $4}'
}

set_brightness() { # $1=bus $2=value
"$DDC" --bus "$1" setvcp 10 "$2" --noverify 2>/dev/null
set_brightness() { # $1=bus $2=value — retries 3x on DDC-CI failure (this LG panel is flaky)
local bus="$1" val="$2" attempt
for attempt in 1 2 3; do
"$DDC" --bus "$bus" setvcp 10 "$val" --noverify 2>/dev/null && return 0
sleep 0.5
done
echo "DDC-CI setvcp failed after 3 attempts on bus $bus (val=$val)" >&2
return 1
}

cancel_timer() {
Expand All @@ -54,8 +61,11 @@ cancel_timer() {
}

schedule_restore() { # $1=bus $2=original_brightness $3=duration
# Write saved brightness so `restore` can read it; use the script itself
# (with retry + 0-default) instead of raw ddcutil.
printf '%s:%s\n' "$1" "$2" > "$STATE"
systemd-run --user --unit="$UNIT" --on-active="$3" --timer-property=AccuracySec=1s \
"$DDC" --bus "$1" setvcp 10 "$2" >/dev/null
"$0" restore >/dev/null
}

# --- actions ---------------------------------------------------------------
Expand All @@ -77,32 +87,43 @@ blackout() {

restore() {
cancel_timer
# Kill any running background fade (sleep was triggered but wake came before it finished)
if [ -f "$FADE_PID_FILE" ]; then
local fpid
fpid=$(cat "$FADE_PID_FILE" 2>/dev/null || true)
rm -f "$FADE_PID_FILE"
if [ -n "$fpid" ] && kill -0 "$fpid" 2>/dev/null; then
kill "$fpid" 2>/dev/null || true
sleep 0.5
fi
fi
local bus="" cur=""
[ -f "$STATE" ] && IFS=: read -r bus cur < "$STATE" || true
bus="${bus:-$(detect_bus || true)}"; cur="${cur:-60}"
bus="${bus:-$(detect_bus || true)}"
# Saved brightness of 0 means blackout read while monitor was already dark
# (race with prior fade) — fall back to a usable default instead of staying dark.
[ -n "$cur" ] && [ "$cur" != "0" ] || cur=60
[ -n "$bus" ] || { echo "no DDC/CI monitor detected" >&2; exit 1; }
set_brightness "$bus" "$cur"
rm -f "$STATE"
echo "Monitor restored (bus $bus -> $cur/100)."
}

fade_to_zero() { # $1=bus $2=from_brightness
local bus="$1" from="$2" to=0 i val
for (( i=0; i<=FADE_STEPS; i++ )); do
local bus="$1" from="$2" to=0 val i
for i in $(seq 0 $FADE_STEPS); do
val=$(( from + (to - from) * i / FADE_STEPS ))
set_brightness "$bus" "$val" &
set_brightness "$bus" "$val"
sleep "$FADE_DELAY"
wait
done
}

fade_from_zero() { # $1=bus $2=to_brightness
local bus="$1" to="$2" from=0 i val
for (( i=0; i<=FADE_STEPS; i++ )); do
local bus="$1" to="$2" from=0 val i
for i in $(seq 0 $FADE_STEPS); do
val=$(( from + (to - from) * i / FADE_STEPS ))
set_brightness "$bus" "$val" &
set_brightness "$bus" "$val"
sleep "$FADE_DELAY"
wait
done
}

Expand All @@ -113,9 +134,11 @@ fade_blackout() {
cur=$(get_brightness "$bus") || true
[ -n "${cur:-}" ] || { echo "could not read brightness on bus $bus" >&2; exit 1; }
printf '%s:%s\n' "$bus" "$cur" > "$STATE"
echo "$$" > "$FADE_PID_FILE"

fade_to_zero "$bus" "$cur"

rm -f "$FADE_PID_FILE"
cancel_timer
schedule_restore "$bus" "$cur" "$dur"
echo "Monitor faded to black (bus $bus, was $cur/100). Auto-restore in $dur — or: $0 restore"
Expand All @@ -125,7 +148,8 @@ fade_restore() {
cancel_timer
local bus="" cur=""
[ -f "$STATE" ] && IFS=: read -r bus cur < "$STATE" || true
bus="${bus:-$(detect_bus || true)}"; cur="${cur:-60}"
bus="${bus:-$(detect_bus || true)}"
[ -n "$cur" ] && [ "$cur" != "0" ] || cur=60
[ -n "$bus" ] || { echo "no DDC/CI monitor detected" >&2; exit 1; }

fade_from_zero "$bus" "$cur"
Expand Down
5 changes: 4 additions & 1 deletion scripts/opencode/opencode.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,10 @@
// guard_core.py's "opencode" policy, which parses argv.
"*git*add -A*": "deny",
"*git*add --all*": "deny",
"*git*add .": "deny",
// `*git*add .` removed 2026-08-05: false-positived on `git add <specific-files>`
// (glob `*` consumed the path, `.` matched any dot). guard_core.py's
// check_git_add_all handles this case via argv parsing (_stages_everything),
// and the remaining deny patterns catch the worst cases.
"*git*stash*": "deny",
"*git*reset --hard*": "deny",
"*git*clean -f*": "deny",
Expand Down
2 changes: 1 addition & 1 deletion scripts/rig-control.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ do_sleep() {
do_wake() {
echo "awake" > "$STATE_FILE"
rgb_on
restore_bg
restore
notify "Wake mode activated"
}

Expand Down