#!/usr/bin/env bash
# Reference PreToolUse guard for Claude Code (and adaptable to other
# tools that pass the pending shell command on stdin as JSON, or via the
# $TOOL_INPUT / $CLAUDE_TOOL_INPUT env var depending on the tool version).
#
# Provided by @abblor/agent-os. Enforces constitution.md section 1 (Safety &
# Git Operations): no unapproved pushes, no repo-local worktrees.
#
# Exit code 0 = allow, non-zero = block the tool call.
#
# ---------------------------------------------------------------------------
# Push approval
# ---------------------------------------------------------------------------
# Earlier versions blocked every push unconditionally. That is stricter than
# the constitution requires - it says "explicit user approval per push", not
# "never" - and it left an approved push with no route through at all: the
# human had to leave the session and run the command by hand every time, which
# in practice meant the agent's work stopped one step short of done.
#
# A push is now allowed when a valid approval token is present. The token is:
#
#   single use    consumed the moment it is examined, so one approval
#                 authorises exactly one attempt
#   time boxed    expires after a short TTL (default 900s)
#   branch bound  recorded against the branch checked out when it was granted,
#                 so approval for one branch cannot authorise another
#   force aware   a force push is only covered if approval was granted with
#                 --force, and force pushing a protected branch is refused
#                 outright, approved or not
#
# Grant one with:
#   npx agent-os approve-push --reason "<what the user actually said>"
#
# THREAT MODEL, STATED PLAINLY. An agent that can run arbitrary shell can also
# run approve-push, so this is not a cryptographic boundary against a
# misaligned agent, and it is not presented as one. What it buys is still
# real: a push can no longer happen as an incidental side effect of some other
# command, each one is a separate deliberate act with the approving words
# recorded, and scope plus expiry bound what a stale approval can do.
#
# For a genuine boundary, set AGENT_OS_PUSH_REQUIRE_TTY=1. Approval must then
# be confirmed on a real terminal, which a non-interactive agent process
# cannot do.

set -euo pipefail

# Claude Code passes hook input as JSON on stdin. Fall back to $1 for
# manual/testing invocation.
INPUT="$(cat 2>/dev/null || true)"
COMMAND="${1:-}"
if [ -z "$COMMAND" ] && [ -n "$INPUT" ]; then
  COMMAND="$(echo "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"command"[[:space:]]*:[[:space:]]*"([^"]*)"/\1/' || true)"
fi

if [ -z "$COMMAND" ]; then
  exit 0
fi

PROTECTED_BRANCHES_REGEX='^(main|master)$'

deny() {
  echo "$1" >&2
  exit 1
}

# ------------------------------------------------------------------- push ---

if echo "$COMMAND" | grep -qE '\bgit\s+push\b'; then
  GIT_DIR="$(git rev-parse --absolute-git-dir 2>/dev/null || true)"
  if [ -z "$GIT_DIR" ]; then
    deny "BLOCKED by agent-os guard-bash.sh: push attempted outside a git repository."
  fi

  TOKEN="$GIT_DIR/agent-os/push-approval"

  if [ ! -f "$TOKEN" ]; then
    deny "BLOCKED by agent-os guard-bash.sh: pushing requires explicit user approval per push (constitution.md section 1).

Ask the user, and once they have approved, record it and retry:

  npx agent-os approve-push --reason \"<what the user actually said>\"

The approval is single use, expires shortly, and covers only the branch that
is checked out now."
  fi

  # Consume on sight. A stale, malformed or mismatched token is still spent,
  # so a rejected attempt can never be retried against the same approval.
  TOKEN_BODY="$(cat "$TOKEN" 2>/dev/null || true)"
  rm -f "$TOKEN"

  field() {
    printf '%s\n' "$TOKEN_BODY" | grep -E "^$1=" | head -1 | cut -d= -f2- || true
  }

  T_EXPIRES="$(field expires_at)"
  T_BRANCH="$(field branch)"
  T_FORCE="$(field allow_force)"

  if ! printf '%s' "$T_EXPIRES" | grep -qE '^[0-9]+$'; then
    deny "BLOCKED by agent-os guard-bash.sh: the push approval token is malformed. It has been discarded; ask for approval again."
  fi

  NOW="$(date +%s)"
  if [ "$NOW" -ge "$T_EXPIRES" ]; then
    deny "BLOCKED by agent-os guard-bash.sh: the push approval has expired. Ask the user to approve again, then re-run approve-push."
  fi

  CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
  if [ -n "$T_BRANCH" ] && [ "$T_BRANCH" != "$CURRENT_BRANCH" ]; then
    deny "BLOCKED by agent-os guard-bash.sh: approval was granted for '$T_BRANCH' but HEAD is on '$CURRENT_BRANCH'. Approval is branch scoped; ask for it again on this branch."
  fi

  if echo "$COMMAND" | grep -qE '(^|[[:space:]])(--force|--force-with-lease|-f)([[:space:]]|=|$)'; then
    if printf '%s' "$CURRENT_BRANCH" | grep -qE "$PROTECTED_BRANCHES_REGEX"; then
      deny "BLOCKED by agent-os guard-bash.sh: force pushing '$CURRENT_BRANCH' is never permitted, approved or not (constitution.md section 1)."
    fi
    if [ "$T_FORCE" != "1" ]; then
      deny "BLOCKED by agent-os guard-bash.sh: this is a force push, and the approval did not cover one. Re-approve with:

  npx agent-os approve-push --force --reason \"...\""
    fi
  fi

  exit 0
fi

# ------------------------------------------------------- repo-local worktrees

if echo "$COMMAND" | grep -qE '(^|[;&|]\s*)mkdir\s+(-p\s+)?(\./)?\.?worktrees?\b'; then
  deny "BLOCKED by agent-os guard-bash.sh: repo-local worktrees are forbidden (constitution.md section 1). Create the worktree outside the repo tree instead."
fi

exit 0
