#!/usr/bin/env bash # snappy-os-inject.sh — Inject program.md + state/index.md + matching skill # loaders into every session. # # Always-injected: # - program.md (the schema) # - state/index.md (the catalog) # # Mention-triggered (ported from snappy-kernel pattern, 2026-04-17): # For each `state/skills/.agents.md` file, if `` appears as a # word in the user prompt, inject that file inside a # block. The .agents.md sidecar carries # distilled rules / hard-won lessons so the next agent doesn't re-discover # them. The full .md skill page remains read-on-demand. # # Convention enforced by `state/lint/check.ts`: every skill .md MUST have a # matching .agents.md sidecar. New skills without distilled rules ship a # stub explicitly stating "no rules yet" — silence is not allowed. # # Fetches from skills.snappy.ai (the gateway), caches locally for 5 minutes. # Falls back to local repo if network is down. # # Works for both: # - UserPromptSubmit → emits additionalContext # - PreToolUse (Task) → prepends to subagent prompt set -euo pipefail GATEWAY="https://skills.snappy.ai/.well-known/skills/snappy-os" CACHE_DIR="${HOME}/.cache/snappy-os" CACHE_TTL=300 # seconds LOCAL_FALLBACK="${HOME}/projects/snappy-os" SKILLS_DIR="${LOCAL_FALLBACK}/state/skills" # Load master key for personal-tier access (snappy-os owns credentials) ENV_CACHE="${HOME}/projects/snappy-os/.env.cache" MASTER_KEY="" if [ -f "$ENV_CACHE" ]; then MASTER_KEY="$(grep '^SNAPPY_MASTER_KEY=' "$ENV_CACHE" | cut -d= -f2- | tr -d '"' | tr -d "'")" fi mkdir -p "$CACHE_DIR" fetch_or_cache() { local file="$1" local cache_file="${CACHE_DIR}/$(echo "$file" | tr '/' '_')" # Use cache if fresh if [ -f "$cache_file" ]; then local age=$(( $(date +%s) - $(stat -f %m "$cache_file") )) if [ "$age" -lt "$CACHE_TTL" ]; then cat "$cache_file" return 0 fi fi # Fetch from gateway local auth_header="" if [ -n "$MASTER_KEY" ]; then auth_header="Authorization: Bearer ${MASTER_KEY}" fi local content if content="$(curl -sS --max-time 3 ${auth_header:+-H "$auth_header"} "${GATEWAY}/${file}" 2>/dev/null)" && [ -n "$content" ]; then printf '%s' "$content" > "$cache_file" printf '%s' "$content" return 0 fi # Fall back to local local local_file="${LOCAL_FALLBACK}/${file}" if [ -f "$local_file" ]; then cat "$local_file" return 0 fi return 1 } # Bail if we can't get program.md from any source program_content="$(fetch_or_cache "program.md")" || exit 0 index_content="$(fetch_or_cache "state/index.md")" || exit 0 input="$(cat)" # Detect mode task_prompt="$(printf '%s' "$input" | jq -r '.tool_input.prompt // empty' 2>/dev/null || true)" user_prompt="$(printf '%s' "$input" | jq -r '.prompt // empty' 2>/dev/null || true)" if [ -n "$task_prompt" ]; then MODE="task" prompt="$task_prompt" elif [ -n "$user_prompt" ]; then MODE="user" prompt="$user_prompt" else exit 0 fi # --- Mention-triggered loader injection --- # For each state/skills/.agents.md, if appears as a word in the # prompt, append the loader to the context block. Word-boundary match avoids # substring false-positives ("fix" inside "prefix" doesn't count). loader_block="" loader_log="" if [ -d "$SKILLS_DIR" ]; then for loader_file in "$SKILLS_DIR"/*.agents.md; do [ -f "$loader_file" ] || continue skill_name="$(basename "$loader_file" .agents.md)" if printf '%s' "$prompt" | grep -qE "(^|[^a-zA-Z0-9_-])${skill_name}([^a-zA-Z0-9_-]|$)"; then # Prefer local file over gateway for sidecars. Gateway returns # {"error":"File not found"} for sidecars not yet pushed, and # fetch_or_cache caches that error string. Local is the canonical # for any machine that has the repo cloned. loader_content="$(cat "$loader_file" 2>/dev/null)" || continue loader_block+=$'\n\n\n'"$loader_content"$'\n' loader_log="${loader_log}${skill_name} " fi done fi context_block=" You are operating snappy-os. The following two files are your operating system. Read them, then route or execute. --- program.md --- ${program_content} --- state/index.md --- ${index_content} ${loader_block}" # Audit log (best-effort, never blocks) if [ -n "$loader_log" ]; then log_dir="${HOME}/.claude/logs" mkdir -p "$log_dir" 2>/dev/null || true printf '[%s] %s-inject loaders:%s\n' "$(date -u +%FT%TZ)" "$MODE" "${loader_log% }" \ >> "$log_dir/snappy-os-inject.log" 2>/dev/null || true fi # Emit if [ "$MODE" = "task" ]; then new_prompt="${context_block}"$'\n\n'"${task_prompt}" printf '%s' "$input" | jq --arg p "$new_prompt" ' { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: (.tool_input | .prompt = $p) } } ' else jq -n --arg ctx "$context_block" '{ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: $ctx } }' fi