{...}: { config.flake.modules.homeManager.homey-alert = { lib, config, pkgs, ... }: { options.azos.homey-alert.enable = lib.mkOption { default = false; example = true; type = lib.types.bool; }; options.azos.homey-alert.longCommandThresholdSeconds = lib.mkOption { default = 300; example = 600; type = lib.types.int; description = '' Installs a global PreToolUse hook (matcher "Bash") in ~/.claude/settings.json that transparently wraps every Bash command Claude Code runs: it times the command, and if it ran at or past this threshold, fires homey-alert with the elapsed time and exit code once it finishes. Commands that finish faster are unaffected. This exists so long-running commands (big rclone transfers, builds) notify automatically even when Claude never thinks to chain homey-alert itself, and even if the harness moves the command to the background partway through. The hook only adds timing + notification around the command as given -- it does not change permissions, so normal confirmation prompts for risky commands still happen exactly as before. ''; }; config = lib.mkIf config.azos.homey-alert.enable (let threshold = toString config.azos.homey-alert.longCommandThresholdSeconds; # Rewrites every Bash tool command to time itself and fire # homey-alert if it ran at or past the threshold. The original # command is spliced in verbatim (not eval'd from an encoded blob) # so it stays human-readable in any permission prompt, and it's run # in a subshell so the wrapper's own timing/notify code still runs # even if the original command uses `set -e` or calls `exit`. # permissionDecision is deliberately omitted from the hook's JSON # output: supplying it (even as "allow") would bypass the normal # confirmation flow, which must stay untouched here. bashNotifyHook = pkgs.writeShellApplication { name = "azos-bash-notify-hook"; runtimeInputs = [pkgs.jq]; text = '' input=$(cat) command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null) || command="" if [ -z "$command" ]; then echo '{}' exit 0 fi wrapped="__azos_notify_start=\$(date +%s) ( $command ) __azos_notify_rc=\$? __azos_notify_dur=\$(( \$(date +%s) - __azos_notify_start )) if [ \"\$__azos_notify_dur\" -ge ${threshold} ]; then __azos_notify_tag=\$([ \"\$__azos_notify_rc\" -eq 0 ] && echo white_check_mark || echo x) homey-alert -t \"Long command\" -T \"\$__azos_notify_tag\" \"Finished in \$__azos_notify_dur s (exit \$__azos_notify_rc)\" >/dev/null 2>&1 || true fi exit \"\$__azos_notify_rc\"" jq -n --arg cmd "$wrapped" \ '{hookSpecificOutput: {hookEventName: "PreToolUse", updatedInput: {command: $cmd}}}' 2>/dev/null || echo '{}' ''; }; in { azos.claude.globalSkills.notify = ./notify-skill.md; home.activation.configureBashNotifyHook = lib.hm.dag.entryAfter ["writeBoundary"] '' SETTINGS_JSON="$HOME/.claude/settings.json" HOOK_ENTRY=$(${pkgs.jq}/bin/jq -n \ --arg cmd "${bashNotifyHook}/bin/azos-bash-notify-hook" \ '{ matcher: "Bash", hooks: [{type: "command", command: $cmd}] }') if [ -f "$SETTINGS_JSON" ]; then tmp=$(mktemp) ${pkgs.jq}/bin/jq --argjson entry "$HOOK_ENTRY" \ '.hooks.PreToolUse = ((.hooks.PreToolUse // []) | map(select(.matcher != $entry.matcher))) + [$entry]' \ "$SETTINGS_JSON" > "$tmp" && mv "$tmp" "$SETTINGS_JSON" else ${pkgs.jq}/bin/jq -n --argjson entry "$HOOK_ENTRY" \ '{hooks: {PreToolUse: [$entry]}}' > "$SETTINGS_JSON" fi ''; home.packages = [ bashNotifyHook (pkgs.writeShellApplication { name = "homey-alert"; runtimeInputs = with pkgs; [curl pass libnotify]; text = '' usage() { cat <&2; usage >&2; exit 1 ;; *) break ;; esac done if [[ $# -gt 0 ]]; then MESSAGE="$*" elif [[ ! -t 0 ]]; then MESSAGE="$(cat)" else echo "homey-alert: no message provided" >&2 usage >&2 exit 1 fi TOKEN="$(pass zakobar-homecenter/ntfy-token)" # Build the curl invocation as an array — single source of truth for both modes CMD=( curl -sf -H "Authorization: Bearer $TOKEN" -H "Priority: $PRIORITY" ) [[ -n "$TITLE" ]] && CMD+=(-H "Title: $TITLE") [[ -n "$TAGS" ]] && CMD+=(-H "Tags: $TAGS") CMD+=(-d "$MESSAGE" https://ntfy.zakobar.com/alerts) if $PRINT_REMOTE; then # Shell-quote each element so the output is safe to embed in SSH commands printf '%q ' "''${CMD[@]}" printf '\n' else "''${CMD[@]}" || true # Local desktop notification, independent of the ntfy push above -- # the server isn't always up, so this always fires as a fallback. URGENCY="normal" case "$PRIORITY" in min|low) URGENCY="low" ;; high|urgent) URGENCY="critical" ;; esac notify-send -u "$URGENCY" "''${TITLE:-homey-alert}" "$MESSAGE" || true fi ''; }) ]; }); }; }