Restructure tea skill into a plugin with a mandatory-login guard
Convert the standalone `tea` skill into a skills-dir plugin so commands are
namespaced and an enforcement hook can ship with it:
- /tea:login — pin the project Gitea login into .claude/settings.local.json
- /tea:use — tea CLI reference (was the old root SKILL.md), with the
login rule slimmed since the hook now enforces it
- hooks/tea-guard.sh — PreToolUse(Bash) guard: blocks any `tea` command that
touches Gitea unless it carries --login and $GITEA_LOGIN is set. Exempts
`tea logins list` and `tea --version/--help` so /tea:login can bootstrap.
References moved under skills/use/references/. `claude plugin validate` passes;
guard unit-tested across allow/block cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/tea-guard.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# tea-guard — PreToolUse hook for Bash commands.
|
||||
#
|
||||
# Enforces, deterministically, the one rule the prose in /tea:use cannot:
|
||||
# every `tea` invocation that touches Gitea MUST carry --login "$GITEA_LOGIN",
|
||||
# and $GITEA_LOGIN must be set. Without this, `tea` silently falls back to the
|
||||
# machine's default login (often the user's personal account) and writes under
|
||||
# the wrong identity.
|
||||
#
|
||||
# Exit codes: 0 = allow, 2 = block (stderr is fed back to Claude as the reason).
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
input="$(cat)"
|
||||
|
||||
# Extract tool_input.command from the hook payload (jq if present, else python3).
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null || true)"
|
||||
else
|
||||
cmd="$(printf '%s' "$input" | /usr/bin/python3 -c 'import sys,json; print(json.load(sys.stdin).get("tool_input",{}).get("command","") or "")' 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
# Not a `tea` command → not our concern.
|
||||
if ! printf '%s' "$cmd" | grep -Eq '(^|[;&|(]|[[:space:]])tea([[:space:]]|$)'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Whitelist: login enumeration + meta. These do not act under any identity and
|
||||
# are needed by /tea:login itself (which runs while $GITEA_LOGIN may be unset).
|
||||
if printf '%s' "$cmd" | grep -Eq 'tea[[:space:]]+(logins[[:space:]]+(list|ls)|--version|-v|--help|help)([[:space:]]|$)'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Require an explicit --login / -l on the invocation.
|
||||
if ! printf '%s' "$cmd" | grep -Eq '(--login|[[:space:]]-l)([[:space:]=])'; then
|
||||
echo "tea-guard: BLOCKED — every 'tea' command must include --login \"\$GITEA_LOGIN\" (or -l). Run /tea:login to pin the project login, then retry. Only 'tea logins list' and 'tea --version/--help' are exempt." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Require $GITEA_LOGIN to be set in the environment.
|
||||
if [ -z "${GITEA_LOGIN:-}" ]; then
|
||||
echo "tea-guard: BLOCKED — \$GITEA_LOGIN is empty/unset, so --login \"\$GITEA_LOGIN\" would expand to nothing and tea would fall back to the default login. Run /tea:login to pin a login (writes .claude/settings.local.json), then restart the session so it is exported." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user