General-purpose coding policy for Baruch's AI agents
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
#!/usr/bin/env bash
# Flag leftover local hygiene before the agent hands off (a Stop gate).
#
# A Claude Code `Stop` hook: at handoff it runs deterministic, universal
# git-hygiene checks and, when it finds clearly-actionable leftovers, blocks the
# stop once with an actionable reason so the agent can clean up. We hand-cleaned
# exactly this mess in real sessions — stray merged-and-remote-deleted local
# branches and an abandoned worktree. This mechanizes the pre-handoff cleanup
# rules/language-diagnostics.md endorses ("a Stop or pre-handoff hook running the
# gate mechanizes this") and complements the fleet-wide delete_branch_on_merge
# (that auto-cleans REMOTE branches; this covers the LOCAL branches/worktrees it
# never touches).
#
# Why nativeHooks (not the portable `hooks` tier): blocking a stop is an
# agent-specific contract with no portable consensus form — the `hooks` tier
# wraps the script in `tessl hook run`, which translates `additionalContext`
# (informational), not a stop-block. nativeHooks writes the entry raw to each
# agent's native config, so the agent reads the script's `{"decision":"block"}`
# directly. Claude Code and Codex share the same Stop contract (decision/reason/
# stop_hook_active), so the SAME script is dual-wired. The two entries differ in
# shape by necessity: Claude Code's config takes command + args[], Codex's takes
# a single command string (its config has no args field), so nativeHooks.codex
# uses `bash "<path>"` as one string. Verified by installing into scratch
# consumers and inspecting .claude/settings.json and .codex/config.toml.
#
# Blocking findings (gate the stop, once):
# - Leftover local branches whose upstream is gone (merged then remote-deleted).
# - Orphaned linked worktrees whose branch's upstream is gone.
# - Diagnostics findings in the CHANGED set only (uncommitted .sh/.py):
# lint the .sh with shellcheck, the .py with pyright. Skipped when nothing
# lintable changed, so a clean handoff costs nothing. An absent engine is
# blocking (the gate can't clear findings without it) — install and re-check.
# Inlined here rather than delegated to scripts/run-diagnostics.sh, which the
# Tessl packer does not ship (only rules/, skills/, hooks/ surfaces publish).
# Report-only (never blocks on its own): a dirty working tree — often intentional
# work-in-progress, surfaced to the user but not trapped.
#
# Loop-safe: reads `stop_hook_active` from stdin and allows immediately when set,
# so it blocks at most once per handoff chain — never traps the agent in a loop.
# Fail-open: any inability to evaluate (no jq, not a git repo, parse failure)
# allows the stop. A hygiene nudge must never wedge a handoff.
#
# Contract:
# stdin : Claude Code Stop JSON. Only `.stop_hook_active` is read.
# stdout: on a block, one JSON object {"decision":"block","reason":"<text>"};
# otherwise nothing.
# exit : always 0 (block is expressed in stdout JSON, never via exit code).
# Best-effort failures warn to stderr and allow the stop.
# state : none — every check reads live git state.
set -euo pipefail
warn() { printf 'stop-handoff-hygiene: %s\n' "$1" >&2; }
# Membership test without associative arrays (macOS ships bash 3.2).
in_list() { # <needle> <haystack...>
local needle="$1" x; shift
for x in "$@"; do [[ "$x" == "$needle" ]] && return 0; done
return 1
}
main() {
local input active inside
local -a gone_branches=() wt_paths=() wt_branches=() leftover=() orphaned=() changed=()
local -a blocking=() reports=()
# jq is required to read stop_hook_active and to emit the block JSON safely.
# Without it we cannot evaluate loop-safety, so fail open (allow the stop).
if ! command -v jq >/dev/null 2>&1; then
warn "jq not found — install jq to enable the handoff-hygiene gate; allowing stop"
return 0
fi
# Read the Stop payload and honor the loop guard: if this stop is already the
# result of our prior block, allow it — never block twice (no loop).
input="$(cat)" || input=""
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)" || active="parse-error"
[[ "$active" == "true" ]] && return 0
[[ "$active" == "parse-error" ]] && { warn "could not parse Stop payload — allowing stop"; return 0; }
command -v git >/dev/null 2>&1 || { warn "git not found — allowing stop"; return 0; }
# Outside a work tree there is nothing to check. `--is-inside-work-tree` prints
# true/false and exits 0 inside any repo; exit 128 is the expected "not a git
# repository" non-result (silent allow). Any other exit is a real failure and
# is surfaced before failing open (rules/error-handling.md). A bare repo /
# gitdir ("false") also allows silently.
rc=0
inside="$(git rev-parse --is-inside-work-tree 2>/dev/null)" || rc=$?
if (( rc != 0 )); then
(( rc == 128 )) || warn "git rev-parse --is-inside-work-tree failed (exit ${rc}) — allowing stop"
return 0
fi
[[ "$inside" == "true" ]] || return 0
collect_gone_branches
collect_worktrees
# Partition gone branches: those checked out in a linked worktree are reported
# as orphaned worktrees (remove the worktree); the rest as leftover branches.
local b p i
for b in "${gone_branches[@]}"; do
in_list "$b" "${wt_branches[@]}" || leftover+=("$b")
done
for (( i = 0; i < ${#wt_paths[@]}; i++ )); do
b="${wt_branches[$i]}"; p="${wt_paths[$i]}"
if in_list "$b" "${gone_branches[@]}"; then orphaned+=("${p} (branch ${b})"); fi
done
build_branch_findings
run_changed_diagnostics
check_dirty_tree
if (( ${#blocking[@]} > 0 )); then
emit_block
elif (( ${#reports[@]} > 0 )); then
# Report-only: surface to the user (stderr) but do not block the handoff.
local r
for r in "${reports[@]}"; do warn "$r"; done
fi
return 0
}
# Populate gone_branches with local branches whose upstream is [gone]. Capture
# for-each-ref's output and exit status so a failure is surfaced, not silently
# read as "no leftovers" (rules/error-handling.md).
collect_gone_branches() {
local out rc=0 name track
out="$(git for-each-ref --format='%(refname:short)%09%(upstream:track)' refs/heads)" || rc=$?
if (( rc != 0 )); then
warn "git for-each-ref failed (exit ${rc}) — skipping the leftover-branch check"
return 0
fi
while IFS=$'\t' read -r name track; do
[[ "$track" == "[gone]" ]] && gone_branches+=("$name")
done <<< "$out"
return 0
}
# Populate wt_paths/wt_branches for LINKED worktrees only (the first porcelain
# record is the main worktree and is skipped). Detached worktrees have no branch
# and are skipped.
collect_worktrees() {
local out rc=0 line key val cur_path="" cur_branch="" first=1
out="$(git worktree list --porcelain)" || rc=$?
if (( rc != 0 )); then
warn "git worktree list failed (exit ${rc}) — skipping the orphaned-worktree check"
return 0
fi
flush() {
if (( first )); then first=0
elif [[ -n "$cur_branch" ]]; then wt_paths+=("$cur_path"); wt_branches+=("$cur_branch"); fi
cur_path=""; cur_branch=""
}
while IFS= read -r line; do
if [[ -z "$line" ]]; then flush; continue; fi
key="${line%% *}"; val="${line#* }"
case "$key" in
worktree) cur_path="$val" ;;
branch) cur_branch="${val#refs/heads/}" ;;
esac
done <<< "$out"
[[ -n "$cur_path" ]] && flush # flush a trailing record with no blank line
return 0
}
# Turn leftover branches and orphaned worktrees into blocking-finding sections.
build_branch_findings() {
local current b p section rc=0
# symbolic-ref exits 1 for the expected detached-HEAD non-result (no current
# branch); any other exit is a real failure and is surfaced.
current="$(git symbolic-ref --quiet --short HEAD 2>/dev/null)" || rc=$?
(( rc == 0 )) || current=""
(( rc == 0 || rc == 1 )) || warn "git symbolic-ref HEAD failed (exit ${rc})"
if (( ${#leftover[@]} > 0 )); then
section="Leftover local branches (merged, upstream deleted) — delete them:"
for b in "${leftover[@]}"; do
if [[ "$b" == "$current" ]]; then
section+=$'\n'" - ${b} (currently checked out — switch away first): git switch <other> && git branch -d ${b}"
else
section+=$'\n'" - ${b}: git branch -d ${b}"
fi
done
blocking+=("$section")
fi
if (( ${#orphaned[@]} > 0 )); then
section="Orphaned worktrees (branch merged, upstream deleted) — remove them:"
for p in "${orphaned[@]}"; do
section+=$'\n'" - ${p}: git worktree remove <path> && git branch -d <branch>"
done
blocking+=("$section")
fi
return 0
}
# Diagnostics on the CHANGED set only: uncommitted .sh/.py files. Skips silently
# when nothing lintable changed. shellcheck the .sh, pyright the .py; findings
# block. A required engine being absent is also blocking (rules/language-
# diagnostics.md Install, Don't Skip — the gate cannot clear findings without it).
run_changed_diagnostics() {
collect_changed_lintable
(( ${#changed[@]} > 0 )) || return 0
local f out
local -a sh_files=() py_files=()
for f in "${changed[@]}"; do
case "$f" in
*.sh) sh_files+=("$f") ;;
*.py) py_files+=("$f") ;;
esac
done
if (( ${#sh_files[@]} > 0 )); then
if command -v shellcheck >/dev/null 2>&1; then
if ! out="$(shellcheck "${sh_files[@]}" 2>&1)"; then
blocking+=("shellcheck findings in changed shell files — fix before handoff:"$'\n'"${out}")
fi
else
blocking+=("shellcheck is not installed but changed .sh files need checking — install shellcheck to clear the pre-handoff diagnostics gate (rules/language-diagnostics.md).")
fi
fi
if (( ${#py_files[@]} > 0 )); then
if command -v pyright >/dev/null 2>&1; then
if ! out="$(pyright "${py_files[@]}" 2>&1)"; then
blocking+=("pyright findings in changed Python files — fix before handoff:"$'\n'"${out}")
fi
else
blocking+=("pyright is not installed but changed .py files need checking — install pyright to clear the pre-handoff diagnostics gate (rules/language-diagnostics.md).")
fi
fi
return 0
}
# Populate `changed` with uncommitted .sh/.py files: tracked changes vs HEAD (or
# the index in an unborn repo) plus untracked files, NUL-safe.
collect_changed_lintable() {
# NUL-safe capture via a temp file so the producer's exit status stays
# observable (a process substitution would hide it); warn on failure and fall
# open to whatever was collected (rules/error-handling.md).
local f tmp rc=0
tmp="$(mktemp)" || { warn "mktemp failed — skipping changed-set diagnostics"; return 0; }
rc=0
if git rev-parse --verify -q HEAD >/dev/null 2>&1; then
git diff --name-only -z --diff-filter=ACMR HEAD -- '*.sh' '*.py' > "$tmp" || rc=$?
else
git diff --name-only -z --diff-filter=ACMR --cached -- '*.sh' '*.py' > "$tmp" || rc=$?
fi
(( rc == 0 )) || warn "git diff failed (exit ${rc}) — changed-set diagnostics may be incomplete"
while IFS= read -r -d '' f; do changed+=("$f"); done < "$tmp"
rc=0
git ls-files -z --others --exclude-standard -- '*.sh' '*.py' > "$tmp" || rc=$?
(( rc == 0 )) || warn "git ls-files failed (exit ${rc}) — untracked changes may be missed"
while IFS= read -r -d '' f; do changed+=("$f"); done < "$tmp"
rm -f "$tmp" || warn "could not remove temp file ${tmp}"
return 0
}
# A dirty working tree is report-only — often intentional WIP, never blocks alone.
check_dirty_tree() {
local status rc=0
status="$(git status --porcelain)" || rc=$?
if (( rc != 0 )); then
warn "git status failed (exit ${rc}) — skipping the dirty-tree report"
return 0
fi
[[ -n "$status" ]] && reports+=("Working tree has uncommitted changes — commit, stash, or discard before handoff.")
return 0
}
# Emit Claude Code's native Stop block decision with the assembled reason.
emit_block() {
local reason section
reason="Pre-handoff hygiene — resolve these local leftovers before finishing:"
for section in "${blocking[@]}"; do reason+=$'\n\n'"${section}"; done
if (( ${#reports[@]} > 0 )); then
local r
for r in "${reports[@]}"; do reason+=$'\n\n'"${r}"; done
fi
jq -n --arg r "$reason" '{decision: "block", reason: $r}' ||
warn "could not emit the block decision as JSON — allowing stop"
return 0
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi.tessl-plugin
hooks
rules
skills
adopt-fork-pr
migrate-to-plugin
onboard-repo