From 97fdb926ac8ea54cf27179e4c12dbb677caa6cb1 Mon Sep 17 00:00:00 2001 From: Aner Zakobar Date: Sun, 11 Oct 2026 23:14:15 +0300 Subject: [PATCH] Auto-notify on long Bash commands; homey-alert local fallback; nextcloud mount fix - homey-alert: add a global PreToolUse hook that transparently times every Bash command Claude runs and fires a notification once a command crosses the configured threshold (default 5 min), without needing the skill to be invoked manually. Also makes homey-alert always fire a local notify-send alongside the ntfy push, since the ntfy server isn't always reachable. - nextcloud: fix sleep crash-loop under the restricted systemd PATH (bin/sleep -> coreutils store path). - azos-core bump (adds ncdu to the base suite). --- azos-core | 2 +- features/homey-alert/default.nix | 101 +++++++++++++++++++++++++-- features/homey-alert/notify-skill.md | 17 +++++ features/nextcloud/default.nix | 2 +- 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/azos-core b/azos-core index 2c7d32e..5669ae0 160000 --- a/azos-core +++ b/azos-core @@ -1 +1 @@ -Subproject commit 2c7d32e10be27ff7b7e19edb53bd4d12ded71beb +Subproject commit 5669ae0692656e7bc99434781009474248ed0e51 diff --git a/features/homey-alert/default.nix b/features/homey-alert/default.nix index 674f4fb..89b5923 100644 --- a/features/homey-alert/default.nix +++ b/features/homey-alert/default.nix @@ -10,20 +10,104 @@ 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. - config = lib.mkIf config.azos.homey-alert.enable { + 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]; + runtimeInputs = with pkgs; [curl pass libnotify]; text = '' usage() { cat <