Skip to content
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.

From plugin
as
38161 skills8 agents2 hooks
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.

  • Matchesstartup${CLAUDE_PLUGIN_ROOT}/hooks/session-start-welcome.sh${CLAUDE_PLUGIN_ROOT}/hooks/version-check.sh

PostToolUse

  • MatchesWrite|Edit${CLAUDE_PLUGIN_ROOT}/hooks/post-write-disclaimer-check.sh
Read hooks/hooks.json

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