# tidy-scan: an agent hook by Scalably

Canonical: https://scalably.io/hooks/tidy-scan
Source: https://github.com/scalably-io/agent-hooks/blob/v1.0.0/hooks/tidy-scan.sh
Runs on: Stop
Release: v1.0.0 at commit 53dcece
Integrity: sha256 of the script served at https://scalably.io/hooks/tidy-scan.sh is cae45129f51543df16e9e084ab8fcd3024422e113ef18966588b1d85209dc8d9. Verify: curl -s https://scalably.io/hooks/tidy-scan.sh | shasum -a 256
This is the machine-readable representation of the page at the canonical URL. Same facts, denser format.

## Summary

Stop hook: surface untidy repository state at the end of a turn.

## Install

```
/plugin marketplace add scalably-io/agent-skills
/plugin install agent-hooks@scalably-agent-skills
```

## What it does

IT SURFACES, IT NEVER DESTROYS. This script only reads. It never deletes,
never checks out, never resets, and never stashes. The rule it enforces is
that an uncommitted file may be real work, so the decision belongs to a human
who can tell the difference between junk and something not finished yet. A
hook that tidies by deleting is a hook that eventually deletes the wrong thing.

It fires at the end of every turn, so it must be fast and quiet. Findings are
hashed per session and reported once; a new finding speaks again.

Reads: Stop JSON on stdin (cwd, session_id)
Exit:  always 0. This hook never blocks a turn.

## The whole script

```bash
#!/usr/bin/env bash
# Stop hook: surface untidy repository state at the end of a turn.
#
# IT SURFACES, IT NEVER DESTROYS. This script only reads. It never deletes,
# never checks out, never resets, and never stashes. The rule it enforces is
# that an uncommitted file may be real work, so the decision belongs to a human
# who can tell the difference between junk and something not finished yet. A
# hook that tidies by deleting is a hook that eventually deletes the wrong thing.
#
# It fires at the end of every turn, so it must be fast and quiet. Findings are
# hashed per session and reported once; a new finding speaks again.
#
# Reads: Stop JSON on stdin (cwd, session_id)
# Exit:  always 0. This hook never blocks a turn.

input=$(cat)
cwd=$(printf '%s' "$input" | jq -r '.cwd // ""' 2>/dev/null) || exit 0
session=$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null) || session="nosession"
[ -n "$cwd" ] || exit 0
[ -d "$cwd" ] || exit 0
cd "$cwd" 2>/dev/null || exit 0

findings=""
add() { findings="${findings}${findings:+$'\n'}- $1"; }

[ -f ".handoff.md" ] && add "\`.handoff.md\` is present. Archive it once its contents are confirmed."

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  root=$(git rev-parse --show-toplevel 2>/dev/null)

  dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
  [ "${dirty:-0}" -gt 0 ] && add "$dirty uncommitted file(s) in $(basename "$root"). Junk, or parked work?"

  if git rev-parse --abbrev-ref '@{upstream}' >/dev/null 2>&1; then
    ahead=$(git rev-list --count '@{upstream}..HEAD' 2>/dev/null)
    [ "${ahead:-0}" -gt 0 ] && add "$ahead unpushed commit(s) on $(git branch --show-current 2>/dev/null)."
  fi

  wt=$(git worktree list --porcelain 2>/dev/null | grep -c '^worktree ')
  [ "${wt:-0}" -gt 1 ] && add "$((wt - 1)) extra worktree(s). Check whether any are merged or stale."
fi

[ -z "$findings" ] && exit 0

# Debounce: identical findings stay silent for the rest of the session.
key=$(printf '%s' "$findings" | shasum 2>/dev/null | cut -d' ' -f1)
stamp="${TMPDIR:-/tmp}/agent-hooks-tidy-${session}"
if [ -f "$stamp" ] && [ "$(cat "$stamp" 2>/dev/null)" = "$key" ]; then
  exit 0
fi
printf '%s' "$key" > "$stamp" 2>/dev/null

jq -n --arg msg "Tidiness scan:"$'\n'"$findings" \
  '{systemMessage: $msg, suppressOutput: true}' 2>/dev/null
exit 0
```
