Hooks
What dotnet-episteme-skills runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add Metalnib/dotnet-episteme-skills > /plugin install dotnet-episteme-skills@dotnet-episteme-marketplace
Ships with dotnet-episteme-skills. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
Bash"${CLAUDE_PLUGIN_ROOT}"/hooks/git-readonly-guard.sh
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/design-state-reload.sh - Matches
compact"${CLAUDE_PLUGIN_ROOT}"/hooks/design-state-reload.sh - Matches
clear"${CLAUDE_PLUGIN_ROOT}"/hooks/design-state-reload.sh - Matches
resume"${CLAUDE_PLUGIN_ROOT}"/hooks/design-state-reload.sh
PreCompact
- Matches
auto - Matches
manual
Where it lives
- hooks/design-state-reload.shRunsGitHub
Read the script
#!/usr/bin/env bash # SessionStart hook (matchers: startup, compact, clear, resume) for the dotnet-refactor pipeline. # On a fresh session, and after compaction/clear/resume, re-inject the active DESIGN state file # so the loop resumes from the artifact instead of from lost conversation history. set -euo pipefail dir="${CLAUDE_PROJECT_DIR:-$PWD}/.episteme" # Newest DESIGN-*.md whose frontmatter is not `status: done`. The status line # must be read from the YAML frontmatter only - the Decision log body carries # status/phase entries too, so a whole-file grep would skip an active loop. # The .episteme/ convention is shared with commands/dotnet-refactor.md. state_file="" for f in "$dir"/DESIGN-*.md; do [ -e "$f" ] || continue # Frontmatter = lines between the line-1 `---` fence and the next `---`. frontmatter="$(awk 'NR==1 && $0!="---"{exit} $0=="---"{n++; next} n==1{print} n>=2{exit}' "$f" 2>/dev/null)" printf '%s\n' "$frontmatter" | grep -Eq '^status:[[:space:]]*done[[:space:]]*$' && continue if [ -z "$state_file" ] || [ "$f" -nt "$state_file" ]; then state_file="$f" fi done [ -z "$state_file" ] && exit 0 # Cap what we inject; the file itself stays the source of truth. content=$(head -c 16384 "$state_file") python3 - "$state_file" <<'PY' "$content" import json, sys path, content = sys.argv[1], sys.argv[2] print(json.dumps({ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": ( f"An active /dotnet-refactor design loop was found at {path}. " f"Resume from the phase and status its frontmatter records; the file is the source of truth, not prior conversation.\n\n" f"{content}" ), } })) PY - hooks/git-readonly-guard.shRunsGitHub
Read the script
#!/usr/bin/env bash # PreToolUse guard: this plugin's worker agents (review, refactor and qa lanes) # may only run read-only git and synopsis commands. Other agents and the main # session pass through untouched. set -euo pipefail INPUT="$(cat)" # Subagent calls carry agent_type on both Claude Code and Codex (>=0.145); # main-session calls don't - skip those without spawning python3. case "$INPUT" in *'"agent_type"'*) ;; *) exit 0 ;; esac PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" python3 - "$INPUT" "$PLUGIN_ROOT" <<'PY' import json, os, re, shlex, sys data = json.loads(sys.argv[1]) agent = data.get("agent_type") or "" # Exact lane sets (Claude namespaced, Codex flat) - a user's unrelated # "review-*"/"qa-*" role must not be restricted. GROUPS = { "review": {"correctness", "performance", "security-observability", "data-messaging", "generalist", "maintainer"}, "refactor": {"cartographer", "tracer", "conformance-auditor", "surveyor"}, "qa": {"acceptance", "reuse-design", "dead-code"}, } matched_group = next((group for group, lanes in GROUPS.items() for p in (f"dotnet-episteme-skills:{group}:", f"{group}-") if agent.startswith(p) and agent[len(p):] in lanes), None) if matched_group is None: # Not one of this plugin's worker agents (main session, other agents, # skills): exit 0 = no opinion, the normal permission flow applies. sys.exit(0) cmd = (data.get("tool_input") or {}).get("command") or "" # No chaining/redirection/substitution - read-only commands need none of it. if re.search(r"[;&|<>`$]", cmd) or "\n" in cmd: print("Blocked for plugin worker agents: shell operators are not allowed.", file=sys.stderr) sys.exit(2) # Every worker group gets the read-only search/list tools on top of git and # synopsis - fast tools (rg, fd) plus the GNU fallbacks, and `command -v`/`which` # to probe which is installed. Review and qa lanes used to be denied these, which # only cost them a run of failed calls before they fell back to the Grep tool: # the native Read/Grep/Glob tools are not seen by this hook at all, so denying the # shell equivalents bought no confinement it did not already lack. # # The shell-operator block above stops chaining/redirection; but several of # these tools can execute or write through their OWN flags with no operator at # all (rg --pre runs a command per file, tree -o writes a file, file -C # compiles one). Each tool's write/exec flags are denied explicitly. Short # flags cluster (-Cm is -C -m, -zi is -z -i), so the short-flag half of each # pattern matches the letter anywhere in a leading cluster; the letter sets are # per-tool so a safe flag on one tool (rg -x = --line-regexp) is not blocked # because it is dangerous on another (fd -x = --exec). `-o` is per-tool too: # rg/grep use it for --only-matching (safe), tree uses it to write (denied). DANGER_FLAGS = { "rg": r"(^|\s)(--pre|--pre-glob|--hostname-bin|--search-zip)(\s|=|$)|(^|\s)-[A-Za-z]*z", "tree": r"(^|\s)(-o|--output)", "file": r"(^|\s)(--compile)(\s|=|$)|(^|\s)-[A-Za-z]*C", "find": r"(^|\s)(-delete|-exec\w*|-ok\w*|-fprint\w*|-fls)(\s|$)", "fd": r"(^|\s)(--exec|--exec-batch)(\s|=|$)|(^|\s)-[A-Za-z]*[xX]", } if matched_group in ("refactor", "review", "qa"): if re.match(r"^\s*(command\s+-v|which)\s", cmd): sys.exit(0) tm = re.match(r"^\s*(rg|fd|grep|ls|eza|cat|head|tail|wc|tree|file|stat|find)\b", cmd) if tm: danger = DANGER_FLAGS.get(tm.group(1)) if danger and re.search(danger, cmd): print(f"Blocked for plugin worker agents: {tm.group(1)} write/exec flags are not allowed.", file=sys.stderr) sys.exit(2) # Confine reads to the project. An absolute path is allowed when it # resolves inside the project directory - the same rule `git -C` already # uses below, and what a worker naturally writes when it knows the repo # root. Parent-directory escapes stay banned outright. NOTE this governs # the SHELL only - the native Read/Grep/Glob tools are not seen by this # hook (see docs/reviewer-restrictions.md), so on Claude Code this is # defence in depth, not an absolute read sandbox. try: tokens = shlex.split(cmd) except ValueError: print("Blocked for plugin worker agents: could not parse the command safely.", file=sys.stderr) sys.exit(2) # Codex has no CLAUDE_PROJECT_DIR; its payload carries the turn cwd instead. proj = os.environ.get("CLAUDE_PROJECT_DIR") or data.get("cwd") def inside_project(path): if not proj: return False real = os.path.realpath(os.path.expanduser(path)) proj_real = os.path.realpath(proj) return real == proj_real or real.startswith(proj_real + os.sep) for tok in tokens[1:]: if tok.startswith("-"): fv = re.search(r"=([~/].*)$", tok) if fv and not inside_project(fv.group(1)): print("Blocked for plugin worker agents: that flag value points outside the project directory.", file=sys.stderr) sys.exit(2) continue if tok.startswith("/") or tok.startswith("~"): if not inside_project(tok): print("Blocked for plugin worker agents: that path is outside the project directory; search within the project or use the Read/Grep tools.", file=sys.stderr) sys.exit(2) continue if tok == ".." or tok.startswith("../") or "/../" in tok or tok.endswith("/.."): print("Blocked for plugin worker agents: parent-directory escapes are not allowed.", file=sys.stderr) sys.exit(2) sys.exit(0) # --output as a whole flag only: git's read-only --output-indicator-{new,old,context} # write nothing and must not be bl
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.
DotNet Episteme Skills - a curated, manual-first .NET AI skills library rooted in systematic knowledge (episteme) and shaped by disciplined craft (techne), designed for engineers who prioritise precision over hype.
Repo: Metalnib/dotnet-episteme-skills

