Business
Hook
Hooks
What as runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add AlpacaLabsLLC/skills-for-architects > /plugin install as@skills-for-architects
Ships with as. Installing the plugin gets these hooks.
What fires, and when
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
- Matches
startup${CLAUDE_PLUGIN_ROOT}/hooks/session-start-welcome.sh${CLAUDE_PLUGIN_ROOT}/hooks/version-check.sh
PostToolUse
- Matches
Write|Edit${CLAUDE_PLUGIN_ROOT}/hooks/post-write-disclaimer-check.sh
Where it lives
- hooks/post-write-disclaimer-check.shRunsGitHub
Read the script
#!/bin/bash # post-write-disclaimer-check.sh # PostToolUse hook (matcher: Write|Edit). Verifies that markdown outputs # claiming to be regulatory (via the architecture-studio:requires-disclaimer # marker) also carry the canonical disclaimer block. # # Contract: # - A regulatory output ends with the canonical disclaimer block followed # by the HTML-comment marker `<!-- architecture-studio:requires-disclaimer -->`. # - This hook is marker-driven: if the marker is present, the canonical # disclaimer text must also be present. If the marker is absent, the # hook stays silent — the skill considered the output non-regulatory. # - "Present" means the marker occupies its own line. Prose that documents # the convention cites the marker inline inside backticks and is not a # regulatory output, so it is correctly ignored. # - Both Write and Edit provide `tool_input.file_path`; Edit carries no # full content, so the hook always reads the file from disk. # - On findings it emits PostToolUse JSON `{"decision": "block", ...}` on # stdout, which surfaces the reason to Claude (stderr + exit 0 is shown # to no one). # - See rules/professional-disclaimer.md for the canonical block. INPUT=$(cat) if command -v jq >/dev/null 2>&1; then if ! FILE_PATH=$(printf '%s' "$INPUT" | jq -er '.tool_input.file_path | strings' 2>/dev/null); then exit 0 fi elif command -v python3 >/dev/null 2>&1; then if ! FILE_PATH=$(printf '%s' "$INPUT" | python3 -c 'import json,sys try: value = json.load(sys.stdin)["tool_input"]["file_path"] if not isinstance(value, str) or not value: raise ValueError("file_path is not a non-empty string") sys.stdout.write(value) except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc: print(f"invalid hook payload: {exc}", file=sys.stderr) raise SystemExit(2)' 2>/dev/null); then exit 0 fi else exit 0 fi # Only check writes/edits to markdown files. [[ "$FILE_PATH" != *.md ]] && exit 0 [ -f "$FILE_PATH" ] || exit 0 [ -s "$FILE_PATH" ] || exit 0 # The marker is an end-of-file sentinel occupying its own line. Matching it # as a whole line (rather than anywhere in the text) is what separates a # marker in USE from a marker being DOCUMENTED: prose that explains the # convention cites it inline, inside backticks, mid-sentence. Substring # matching blocked every edit to CHANGELOG.md, PATTERNS.md, hooks/README.md, # rules/README.md, and skills/skill-maker/SKILL.md — none of which is a # regulatory output. MARKER_RE='^[[:space:]]*<!-- architecture-studio:requires-disclaimer -->[[:space:]]*$' DISCLAIMER_PHRASE='AI-generated analysis for preliminary planning purposes' # If no marker, this isn't a regulatory output; nothing to check. if ! grep -qE "$MARKER_RE" "$FILE_PATH"; then exit 0 fi PROBLEMS="" # Marker present → canonical disclaimer text must also be present. if ! grep -qF "$DISCLAIMER_PHRASE" "$FILE_PATH"; then PROBLEMS="$FILE_PATH carries the architecture-studio:requires-disclaimer marker but is missing the canonical disclaimer block. Restore the block from ${CLAUDE_PLUGIN_ROOT:-<plugin-root>}/rules/professional-disclaimer.md." fi # Marker should be a single end-of-file sentinel; flag duplicates. MARKER_COUNT=$(grep -cE "$MARKER_RE" "$FILE_PATH") if [ "$MARKER_COUNT" -gt 1 ]; then [ -n "$PROBLEMS" ] && PROBLEMS="$PROBLEMS " PROBLEMS="${PROBLEMS}$FILE_PATH contains the architecture-studio:requires-disclaimer marker $MARKER_COUNT times. It must appear exactly once, at end of file." fi if [ -n "$PROBLEMS" ]; then if command -v jq >/dev/null 2>&1; then jq -n --arg reason "$PROBLEMS" '{decision: "block", reason: $reason}' elif command -v python3 >/dev/null 2>&1; then PROBLEMS="$PROBLEMS" python3 -c 'import json,os; print(json.dumps({"decision":"block","reason":os.environ["PROBLEMS"]}))' else printf '{"decision":"block","reason":"Arch Studio disclaimer check failed; restore the canonical block from %s/rules/professional-disclaimer.md."}\n' "${CLAUDE_PLUGIN_ROOT:-<plugin-root>}" fi fi exit 0 - hooks/session-start-ambient.shGitHub
Read the script
#!/bin/sh # Static local context; no workspace mutation or network access. printf '%s\n' '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "Use Arch Studio for architecture and firm work; Norma coordinates when useful and direct skill selection remains valid. Resolve studio or project context only when the task needs durable records. Use rules/moments.md for relevant state guidance without delaying a quick task or repeating a declined setup offer."}}' || true - hooks/session-start-welcome.shRunsGitHub
Read the script
#!/bin/sh # Each-session discovery; never blocks work or writes an onboarding marker. PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}" if [ -n "$PLUGIN_ROOT" ] && [ -f "$PLUGIN_ROOT/.claude-plugin/plugin.json" ] && [ -f "$PLUGIN_ROOT/skills/norma/SKILL.md" ] && [ -f "$PLUGIN_ROOT/skills/tool-catalog/SKILL.md" ]; then printf '%s\n' '{"systemMessage": "Arch Studio: describe your architecture or firm work, say Norma for coordination, or ask for help.", "hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "Use Arch Studio for architecture and firm work; Norma coordinates when useful and direct skill selection remains valid. Resolve studio or project context only when the task needs durable records. Use rules/moments.md for relevant state guidance without delaying a quick task or repeating a declined setup offer."}}' else printf '%s\n' '{"systemMessage": "Arch Studio package discovery is incomplete; verify the installed package before claiming available workflows.", "hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "Use Arch Studio for architecture and firm work; Norma coordinates when useful and direct skill selection remains valid. Resolve studio or project context only when the task needs durable records. Use rules/moments.md for relevant state guidance without delaying a quick task or repeating a declined setup offer."}}' fi exit 0 - hooks/version-check.shRunsGitHub
Read the script
#!/usr/bin/env bash # Daily release check and anonymous usage ping, both on by default. It must # never block or delay a session after its bounded request. Opt-outs: # AS_UPDATE_CHECK_DISABLED=1 or /as:studio updates disable -> no update notice # AS_TELEMETRY_DISABLED=1 -> request carries no install ID or version # With both off it performs no I/O. set -u DATA_DIR="${ARCHITECTURE_STUDIO_STATE_DIR:-$HOME/.claude}" DISABLED="$DATA_DIR/.architecture-studio-update-check-disabled" INSTALL_ID="$DATA_DIR/.architecture-studio-install-id" CACHE="$DATA_DIR/.architecture-studio-version-check" ENDPOINT="https://version.alpa.llc/" updates=1 if [ "${AS_UPDATE_CHECK_DISABLED:-}" = 1 ] || [ -f "$DISABLED" ]; then updates=0; fi ping=1 [ "${AS_TELEMETRY_DISABLED:-}" = 1 ] && ping=0 (( updates || ping )) || exit 0 mkdir -p "$DATA_DIR" 2>/dev/null || exit 0 umask 077 now=$(date +%s 2>/dev/null) || exit 0 case "$now" in ''|*[!0-9]*) exit 0 ;; esac read_key() { key=$1 [ -f "$CACHE" ] || return 0 grep -E "^${key}=" "$CACHE" 2>/dev/null | tail -n 1 | cut -d= -f2- } write_cache() { checked=$1 remote_value=$2 nudged=$3 tmp=$(mktemp "$DATA_DIR/.architecture-studio-version-check.XXXXXX" 2>/dev/null) || return 1 if ! printf 'checked_at=%s\nremote=%s\nnudged_for=%s\n' "$checked" "$remote_value" "$nudged" > "$tmp"; then rm -f "$tmp" return 1 fi chmod 600 "$tmp" 2>/dev/null || true mv "$tmp" "$CACHE" 2>/dev/null || { rm -f "$tmp"; return 1; } } valid_version() { [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] } version_gt() { valid_version "$1" && valid_version "$2" || return 1 IFS=. read -r a1 a2 a3 a4 <<< "$1" IFS=. read -r b1 b2 b3 b4 <<< "$2" a1=$((10#$a1)); a2=$((10#$a2)); a3=$((10#$a3)); a4=$((10#${a4:-0})) b1=$((10#$b1)); b2=$((10#$b2)); b3=$((10#$b3)); b4=$((10#${b4:-0})) (( a1 > b1 )) || { (( a1 == b1 && a2 > b2 )) || { (( a1 == b1 && a2 == b2 && a3 > b3 )) || { (( a1 == b1 && a2 == b2 && a3 == b3 && a4 > b4 )); }; }; } } parse_remote() { payload=$1 if command -v jq >/dev/null 2>&1; then printf '%s' "$payload" | jq -er '.version | strings' 2>/dev/null else printf '%s' "$payload" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 fi } local_version() { manifest="${CLAUDE_PLUGIN_ROOT:-}/.claude-plugin/plugin.json" [ -f "$manifest" ] || return 1 if command -v jq >/dev/null 2>&1; then jq -er '.version | strings' "$manifest" 2>/dev/null else sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -n 1 fi } # Random, generated once; derived from nothing about the user or machine. install_id() { id=$(cat "$INSTALL_ID" 2>/dev/null) || id="" if ! [[ $id =~ ^[0-9a-f]{32}$ ]]; then id=$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n') [[ $id =~ ^[0-9a-f]{32}$ ]] || return 1 printf '%s\n' "$id" > "$INSTALL_ID" 2>/dev/null || return 1 fi printf '%s' "$id" } local=$(local_version 2>/dev/null) || local="" local=${local#v} valid_version "$local" || local="" # Cache absence is the first-enabled-session boundary. This deliberately does # not depend on the welcome hook or hook ordering. if [ ! -f "$CACHE" ]; then write_cache "$now" "" "" >/dev/null 2>&1 || true exit 0 fi checked_at=$(read_key checked_at) remote=$(read_key remote) nudged_for=$(read_key nudged_for) case "$checked_at" in ''|*[!0-9]*) checked_at=0 ;; esac valid_version "$remote" || remote="" valid_version "$nudged_for" || nudged_for="" if (( now - checked_at >= 86400 || now < checked_at )); then url=$ENDPOINT if (( ping )) && id=$(install_id); then url="$ENDPOINT?id=$id&v=$local" fi payload=$(curl -fsS --max-time 2 "$url" 2>/dev/null) || payload="" fetched=$(parse_remote "$payload" 2>/dev/null) || fetched="" fetched=${fetched#v} if valid_version "$fetched"; then remote=$fetched fi checked_at=$now write_cache "$now" "$remote" "$nudged_for" >/dev/null 2>&1 || true fi (( updates )) || exit 0 [ -n "$local" ] || exit 0 [ -n "$remote" ] || exit 0 version_gt "$remote" "$local" || exit 0 [ "$nudged_for" != "$remote" ] || exit 0 message="Arch Studio $remote is available (you have $local). Update through /plugin → skills-for-architects → update. Disable checks with /as:studio updates disable." context="[architecture-studio update] Installed version: $local. Available version: $remote. The user has already received the update notice; do not bring this up unprompted." if command -v jq >/dev/null 2>&1; then jq -n --arg message "$message" --arg ctx "$context" '{systemMessage:$message,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$ctx}}' || exit 0 else escaped_message=$(printf '%s' "$message" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') escaped_context=$(printf '%s' "$context" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') printf '{"systemMessage":"%s","hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$escaped_message" "$escaped_context" || exit 0 fi write_cache "$checked_at" "$remote" "$remote" >/dev/null 2>&1 || true exit 0
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withas
Claude Code skills for architecture, real estate, and workplace strategy. Type /skill-name and go.
Get the whole plugin
Stats
381
Stars
81
Forks
Active
Maintenance
Python
Language
MIT
License
1d ago
Last commit
7mo ago
Created
3h ago
Added

