Content
Hook
Hooks
What academic-research-skills 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 Imbad0202/academic-research-skills > /plugin install academic-research-skills@academic-research-skills
Ships with academic-research-skills. 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.
bash "${CLAUDE_PLUGIN_ROOT}/scripts/announce-ars-loaded.sh"
PreToolUse
- Matches
Write|Edit|MultiEdit|Bashbash "${CLAUDE_PLUGIN_ROOT}/hooks/run_guard.sh"
Where it lives
- hooks/run_guard.shRunsGitHub
Read the script
#!/bin/sh # version: 1.0.1 # # ARS write-scope guard LAUNCHER — PreToolUse hook (#454 Windows portability fix). # # WHY THIS EXISTS: the guard hook used to be wired as `python3 ".../ars_write_scope_guard.py"` # directly. On Windows, `python3` is commonly a 0-byte Microsoft Store App Execution Alias # stub (not real Python); invoking it non-interactively fails BEFORE the guard's Python runs, # so none of the guard's own fail-safes apply — it just errors and spams the hook log (#454). # # This launcher finds a REAL Python (skipping stubs), then runs the guard as a SUPERVISED # subprocess. Design: docs/design/2026-06-17-454-windows-python-hook-portability-design.md. # # POSTURE (Plan A — graceful degradation; the guard is OPTIONAL v3.10 hardening and ARS core # needs no Python): if no real Python is found, OR the guard subprocess misbehaves, the # launcher emits a valid PASS-THROUGH hook JSON and exits 0. It NEVER exits non-zero on these # degraded paths (a non-2 exit blocks nothing anyway and only spams logs; exit 2 would # hard-lock the user out of all writes/Bash for an environment gap or an ARS-side bug — wrong # for an optional layer). It stays SILENT on stderr on degraded paths: PreToolUse is a hot # path, so any per-call stderr IS the spam #454 is about. # # Bash 3.2 / POSIX sh compatible (same constraint as scripts/announce-ars-loaded.sh). On # Windows this runs under Git Bash; with no Git Bash, CC falls back to PowerShell which can't # run this .sh — the guard is then inactive (accepted degradation, see spec §3.3). # Canonical pass-through output: no permissionDecision => falls back to the normal permission # flow (NEVER emit "allow" — that would skip every other permission rule). PASS_THROUGH='{"hookSpecificOutput":{"hookEventName":"PreToolUse"}}' emit_passthrough_and_exit() { printf '%s\n' "$PASS_THROUGH" exit 0 } # --- Resolve the guard script from THIS launcher's own location (codex P1) --------------- # CC substitutes ${CLAUDE_PLUGIN_ROOT} into the hook COMMAND text before the shell, but does # NOT guarantee it as an env var inside this script. So compute the guard path from $0. # (No production env override: the guard path is ALWAYS derived from the launcher's own # location. Tests that need a broken/alternate guard run the launcher from a temp plugin # layout, so there is no production back door — P2-e.) # shellcheck disable=SC1007 # `CDPATH= cd` is intentional: clear CDPATH for this one cd only SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || emit_passthrough_and_exit GUARD="$SELF_DIR/../scripts/ars_write_scope_guard.py" # --- Read the payload from stdin ONCE (we must replay it to the guard subprocess) --------- # Known trade-off (gemini round-6 P2, accepted): the payload is held in a shell variable and # replayed with `printf '%s'`. `printf` is a builtin so it is NOT bound by ARG_MAX, and the # stripped trailing newline is irrelevant to the guard's JSON parse, so for ordinary hook # payloads this is safe. A multi-megabyte Write payload on a POSIX shell that caps variable # length is the narrow case this does not cover; buffering to a private temp file would handle # it but adds another temp-file lifecycle (and symlink surface) to a hot path, so it is left as # documented degradation rather than fixed speculatively. PAYLOAD=$(cat) # --- Marker probe: does this candidate run real Python? ---------------------------------- # A candidate is "real" iff the probe exits 0 AND prints the exact marker on stdout. A 0-byte # Store stub fails to execute / prints nothing, so it is skipped. We bound each probe so a # broken-but-hanging interpreter can't wedge the hot path (spec §3.3): prefer `timeout` when # present, else a portable process-group watchdog. MARKER=ARS_PY_OK # Per-candidate (and guard) wall-clock bound, seconds. A small ops knob; validated to be a # bare integer so it can't smuggle anything into the `timeout`/`sleep` args. Default 3. PROBE_BOUND=${ARS_PROBE_BOUND:-3} case "$PROBE_BOUND" in ''|*[!0-9]*) PROBE_BOUND=3 ;; esac # ARS_GUARD_FORCE_WATCHDOG=1 forces the no-`timeout` watchdog path even on hosts that HAVE # `timeout`. This is a test/debug switch: it only changes the BOUNDING MECHANISM (timeout # binary vs process-group watchdog), never the security decision — both paths enforce the # same wall-clock bound and the same pass-through-on-overrun posture. Honored if set; safe # to leave unset (the default prefers the `timeout` binary when present). have_timeout() { [ -z "${ARS_GUARD_FORCE_WATCHDOG:-}" ] && command -v timeout >/dev/null 2>&1 } # Reserved exit status meaning "the bounded command was killed for overrunning its bound". TIMEOUT_STATUS=124 # Run "$@" with a wall-clock bound; its stdout flows to OUR stdout (capture via $(...)). # Returns the command's real exit status, or $TIMEOUT_STATUS if it overran the bound. # Stdin for "$@" is whatever the caller arranges (we redirect it per call site) — the guard # call site PIPES the payload in, so the bounded command MUST keep that stdin. run_bounded() { if have_timeout; then # GNU timeout exits 124 on timeout — normalize to our sentinel for a uniform caller. timeout "${PROBE_BOUND}s" "$@" _st=$? [ "$_st" -eq 124 ] && return "$TIMEOUT_STATUS" return "$_st" fi # No `timeout` binary: a portable background-and-watchdog fallback. Several POSIX subtleties # bite here, all of which broke a naive version (#454 dual-track): # # 1. STDIN: a backgrounded (`&`) command's stdin defaults to /dev/null, which would feed # the GUARD an EMPTY payload (guard reads nothing -> pass-through -> a real `deny` is # LOST and the guard is dead on any timeout-less host). We stash this function's stdin # on fd 3 and redirect the job's stdin from it (`<&3`) so the piped payload arrives. # 2. STDOUT: capturing the job's stdout through the `$(...)` pipe directly would WEDGE the # pipe — if the job spawns a grandchil
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 withacademic-research-skills
A comprehensive suite of Claude Code skills for academic research, covering the full pipeline from research to publication.
Get the whole plugin
Stats
41,490
Stars
3,298
Forks
Active
Maintenance
Python
Language
2h ago
Last commit
5mo ago
Created
Repo: Imbad0202/academic-research-skills

