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).
This commit is contained in:
+1
-1
Submodule azos-core updated: 2c7d32e10b...5669ae0692
@@ -10,20 +10,104 @@
|
|||||||
example = true;
|
example = true;
|
||||||
type = lib.types.bool;
|
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;
|
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 = [
|
home.packages = [
|
||||||
|
bashNotifyHook
|
||||||
(pkgs.writeShellApplication {
|
(pkgs.writeShellApplication {
|
||||||
name = "homey-alert";
|
name = "homey-alert";
|
||||||
runtimeInputs = with pkgs; [curl pass];
|
runtimeInputs = with pkgs; [curl pass libnotify];
|
||||||
text = ''
|
text = ''
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: homey-alert [OPTIONS] [MESSAGE]
|
Usage: homey-alert [OPTIONS] [MESSAGE]
|
||||||
|
|
||||||
Send a notification to the homey ntfy server (ntfy.zakobar.com/alerts).
|
Send a notification to the homey ntfy server (ntfy.zakobar.com/alerts),
|
||||||
|
and (when run locally, not with --print-remote) also via notify-send --
|
||||||
|
the ntfy server isn't always reachable, so the local desktop notification
|
||||||
|
always fires too, independent of whether the ntfy push succeeds.
|
||||||
MESSAGE can be given as a positional argument or piped via stdin.
|
MESSAGE can be given as a positional argument or piped via stdin.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@@ -88,7 +172,16 @@
|
|||||||
printf '%q ' "''${CMD[@]}"
|
printf '%q ' "''${CMD[@]}"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
else
|
else
|
||||||
"''${CMD[@]}"
|
"''${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
|
fi
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,23 @@ When running (or suggesting) a command that will take more than ~30 seconds —
|
|||||||
builds, deploys, downloads, test suites — append a notification automatically. Don't wait to
|
builds, deploys, downloads, test suites — append a notification automatically. Don't wait to
|
||||||
be asked.
|
be asked.
|
||||||
|
|
||||||
|
## Background / async tasks (Claude Code specifically)
|
||||||
|
|
||||||
|
If you (the agent) are running a command via Bash that could take longer than
|
||||||
|
~10 minutes — large rclone/rsync transfers, builds, training runs — and it
|
||||||
|
might run in the background (either because you set `run_in_background`, or
|
||||||
|
because the harness auto-backgrounds it after its timeout), chain the
|
||||||
|
`homey-alert` call **into that same command string from the start**. Don't
|
||||||
|
wait until you notice it's taking a while and then think about notifying —
|
||||||
|
by then the command is already running without it, and the harness's own
|
||||||
|
"task finished" signal only reaches this session, not Aner's phone. Chaining
|
||||||
|
upfront is the only way to get a real ntfy push the moment it actually
|
||||||
|
finishes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone copy ... ; homey-alert -t "Backup" -p default -T "$([ $? -eq 0 ] && echo white_check_mark || echo x)" "rclone copy finished (exit $?)"
|
||||||
|
```
|
||||||
|
|
||||||
## Local commands
|
## Local commands
|
||||||
|
|
||||||
Append after the command so it fires on success only (`&&`) or always (`;`):
|
Append after the command so it fires on success only (`&&`) or always (`;`):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
mountpoint="$HOME/${mountDir}"
|
mountpoint="$HOME/${mountDir}"
|
||||||
mkdir -p "$mountpoint"
|
mkdir -p "$mountpoint"
|
||||||
while ! ${pkgs.azos-card-present}/bin/azos-card-present; do
|
while ! ${pkgs.azos-card-present}/bin/azos-card-present; do
|
||||||
sleep 10
|
${pkgs.coreutils}/bin/sleep 10
|
||||||
done
|
done
|
||||||
password=$(${pkgs.pass}/bin/pass zakobar.com | head -1)
|
password=$(${pkgs.pass}/bin/pass zakobar.com | head -1)
|
||||||
uid=$(${pkgs.pass}/bin/pass nextcloud-username/aner | head -1)
|
uid=$(${pkgs.pass}/bin/pass nextcloud-username/aner | head -1)
|
||||||
|
|||||||
Reference in New Issue
Block a user