| name | env-check |
| description | Use when the user invokes /env-check or asks to verify Claude Code env setup in the current repo. Audits per-language LSP plugin enablement (both directions), a linting pre-commit hook, project-local memory, hook-driven git-worktree isolation readiness (a WorktreeCreate hook that creates .worktrees/<branch> and copies .worktreeinclude literally, split provisioning — ops/setup-env.sh with branch-derived ports + COMPOSE_PROJECT_NAME and ops/seed-db.sh with a .seeded-gated DB seed — wired into make up, WorktreeRemove teardown, a PreToolUse cross-worktree edit blocker), and relevance of enabled plugins/MCP servers to the project. Detection runs in parallel Opus subagents; fixes are applied in the main thread. |
env-check
Audit the current repo's Claude Code environment against a small set of rules.
Detection for every check runs in its own read-only Opus subagent, in parallel.
The main thread aggregates results, then prompts and applies fixes (Rules 1–3) or
prints advisory findings (Rule 4). Fail loudly on missing dependencies or malformed
subagent output — no degraded modes, no loose error handling.
Trigger: /env-check (or "check env", "verify env setup")
Scope: Rules 1–2 cover Python, Go, TypeScript only. Rules 3–4 apply to every repo.
Architecture
main thread: cheap upfront detection (languages, git, compose)
→ fan out: one Explore/opus subagent per applicable check (single message, parallel)
→ each subagent reads its checks/<id>.md spec + the repo → returns strict JSON
→ main thread: parse JSON (abort loud on failure) → report
→ Rules 1–3: prompt y/n, apply fixes in main thread (re-read files fresh)
→ Rule 4: print findings + recommendations only (NEVER writes)
Step 0 — Upfront detection (main thread)
Use Read/Glob for existence; Bash only where noted. Compute:
-
REPO_ROOT — absolute path of the current repo (git rev-parse --show-toplevel, else cwd).
-
IS_GIT_REPO — does REPO_ROOT/.git/ exist.
-
DETECTED_LANGUAGES — from manifest files anywhere in the repo tree (root AND
subfolders — monorepos put frontend/package.json, services/api/go.mod, etc.):
| Language | Detected when ANY match exists anywhere in tree |
|---|
| python | pyproject.toml, requirements.txt, setup.py, Pipfile |
| go | go.mod |
| typescript | tsconfig.json, OR any package.json containing the string "typescript" |
Search scope: recurse from REPO_ROOT, but EXCLUDE dependency/build/vendored
dirs (their manifests are false positives): .git/, .worktrees/, .claude/worktrees/,
node_modules/, vendor/, .venv/, venv/, __pycache__/, dist/, build/,
target/, .next/.
Method: if IS_GIT_REPO, list candidates with
git -C REPO_ROOT ls-files --cached --others --exclude-standard (tracked AND
untracked-but-not-gitignored — so uncommitted manifests still count, gitignored
deps don't). Otherwise use Glob. In BOTH cases, additionally drop any path whose
components include an excluded dir above (backstop for deps that aren't gitignored).
Match manifest basenames against the survivors. For the TypeScript content test,
Grep the literal "typescript" restricted to surviving package.json files.
A language enters scope if matched in ANY folder.
-
COMPOSE_FILE — first of docker-compose.yml, docker-compose.yaml, compose.yml, compose.yaml at repo root; else none.
Step 1 — Decide which checks apply
| Check | Subagent spec | Applies when |
|---|
| rule1-lsp | checks/rule1-lsp.md | always (flags missing AND stale/unneeded LSP plugins) |
| rule2-lint-hook | checks/rule2-lint-hook.md | always |
| rule3-memory | checks/rule3-memory.md | always |
| rule5-relevance | checks/rule5-relevance.md | always (advisory; na if no project plugins/MCPs) |
| wt1-compose | checks/wt1-compose.md | IS_GIT_REPO (returns na if no compose) |
| wt2-worktreeinclude | checks/wt2-worktreeinclude.md | IS_GIT_REPO |
| wt3-provisioning | checks/wt3-provisioning.md | IS_GIT_REPO |
| wt4-db-fixtures | checks/wt4-db-fixtures.md | IS_GIT_REPO (returns na if no compose datastore AND no SQLite file) |
| wt5-worktree-hooks | checks/wt5-worktree-hooks.md | IS_GIT_REPO (returns na if nothing to provision) |
| wt6-edit-isolation | checks/wt6-edit-isolation.md | IS_GIT_REPO (returns na if no worktree workflow) |
Rules 1–3 always run; worktree checks need a git repo. There is no "nothing to
check" early-exit — rule1 runs even with no detected languages (to catch stale LSP
plugins enabled for absent languages).
Step 2 — Dispatch subagents (parallel)
Spawn ALL applicable checks in a single message (multiple Agent calls) so they
run concurrently. For each:
-
subagent_type: Explore (read-only — cannot write files)
-
model: opus
-
prompt:
Read the check spec at ${CLAUDE_PLUGIN_ROOT}/skills/env-check/checks/<id>.md.
Perform that check, read-only, against this target.
Inputs:
REPO_ROOT = <REPO_ROOT>
DETECTED_LANGUAGES = <list> # rule1 + rule5, omit for others
IS_GIT_REPO = <bool> # rule2
COMPOSE_FILE = <path|none> # wt1, wt2, wt3, wt4
Return ONLY the JSON object the spec describes. No prose, no code fences.
Pass only the inputs each check declares. Resolve ${CLAUDE_PLUGIN_ROOT} to its
absolute value in the prompt.
Step 3 — Aggregate
For each subagent result, parse the JSON.
- Parse failure / missing required keys → abort the skill loudly:
error: <check> returned malformed output — aborting and STOP. Do not guess.
- Any result whose
summary starts with ERROR: (e.g. invalid settings.json)
→ print it and STOP the entire skill (no further checks or fixes).
Step 4 — Report
Print one block per check, grouped: Rules 1–3 first (interactive-fix), then the
advisory rules — Rule 4 (worktree) and Rule 5 (plugin/MCP relevance).
Use ✓ for pass, ✗ for fail, — for na, listing each finding's detail.
Rule 1 — LSP plugins
✓ pyrefly-lsp@suso-plugins enabled
Rule 2 — Linting pre-commit hook
✗ no linting pre-commit hook found
Rule 3 — Project memory
✗ autoMemoryDirectory not set in .claude/settings.local.json
Rule 4 — Worktree isolation
✗ wt1 compose: container_name set on service "db"
✗ wt2 frontend/.env.local gitignored but not covered by .worktreeinclude
✗ wt3 ops/setup-env.sh missing
✗ wt4 db: db service present but ops/seed-db.sh missing
✗ wt5 no WorktreeCreate hook registered
✗ wt6 no PreToolUse hook blocks cross-worktree edits
Rule 5 — Plugin/MCP relevance
✗ postgres MCP enabled but project has no database
Step 5 — Fixes for Rules 1–3 (interactive, main thread)
For each ✗ in Rules 1–3 with an available fix, prompt (y/n) individually. Before
writing, re-read the relevant file fresh — never trust subagent-reported state
for a write. Apply only confirmed fixes.
Rule 1 fix — align LSP plugins (one prompt per finding)
[missing] finding — prompt Enable <key>? (y/n). On y:
- If
.claude/settings.json doesn't exist, create .claude/ and an empty {}.
- Read, parse, set
enabledPlugins[<key>] = true (create enabledPlugins if absent), write with 2-space indent, preserving all other keys.
- Confirm:
→ enabled <key> in .claude/settings.json.
[unneeded] finding — prompt Disable <key>? No <lang> in the project. (y/n). On y:
- Read, parse, delete the
<key> entry from enabledPlugins (do not set false — remove it). Preserve all other keys, including non-managed plugins.
- Write with 2-space indent.
- Confirm:
→ removed <key> from .claude/settings.json.
Rule 2 fix — native git pre-commit hook (only if IS_GIT_REPO)
Not a git repo → no fix offered (subagent already reported this). Otherwise prompt:
Create native git pre-commit hook at .git/hooks/pre-commit? (y/n). On y:
-
Assemble the script with ONLY the blocks for DETECTED_LANGUAGES:
| Language | Block |
|---|
| python | uvx ruff check on staged *.py |
| go | gofmt -l (fail if unformatted) then golangci-lint run |
| typescript | npx --no-install prettier --check then npx --no-install eslint on staged *.ts/*.tsx |
#!/bin/sh
set -e
staged() { git diff --cached --name-only --diff-filter=ACM -- "$@"; }
py=$(staged '*.py')
if [ -n "$py" ]; then
echo "$py" | xargs uvx ruff check
fi
go=$(staged '*.go')
if [ -n "$go" ]; then
bad=$(echo "$go" | xargs gofmt -l)
[ -z "$bad" ] || { echo "gofmt needed:"; echo "$bad"; exit 1; }
golangci-lint run
fi
ts=$(staged '*.ts' '*.tsx')
if [ -n "$ts" ]; then
echo "$ts" | xargs npx --no-install prettier --check
echo "$ts" | xargs npx --no-install eslint
fi
-
If .git/hooks/pre-commit already exists, warn: .git/hooks/pre-commit exists — overwrite? (y/n). On n, skip.
-
Write the assembled script, then chmod +x .git/hooks/pre-commit.
-
Confirm: → wrote .git/hooks/pre-commit (executable). Ensure the linters for detected languages are installed (commits fail loudly otherwise).
Rule 3 fix — configure project memory
The subagent's finding carries TARGET, DEFAULT, SETTING_PATH, and whether each
folder exists. Prompt: Configure project memory? (y/n). On y, act by state:
| TARGET exists | DEFAULT exists | Action |
|---|
| no | yes | mv "$DEFAULT" "$TARGET" → → moved memory from <DEFAULT> to <TARGET> |
| no | no | mkdir -p "$TARGET" → → created empty <TARGET> |
| yes | no | → <TARGET> already exists |
| yes | yes | conflict prompt below |
Both-exist conflict prompt:
Memory exists in both locations:
- <TARGET> (<N> files)
- <DEFAULT> (<M> files)
k) Keep <TARGET>; archive <DEFAULT> as <DEFAULT>.bak-<YYYYMMDD-HHMMSS>
d) Use <DEFAULT>; archive <TARGET> as <TARGET>.bak-<ts>, then move DEFAULT into place
s) Skip — leave both, don't touch settings.local.json
Choice (k/d/s):
k → mv "$DEFAULT" "$DEFAULT.bak-<ts>". d → mv "$TARGET" "$TARGET.bak-<ts>" then mv "$DEFAULT" "$TARGET". s → print Skipped — Rule 3 not applied. and skip the settings write.
After folder is in place (any path but s):
- Read
.claude/settings.local.json (or {}).
- Set
autoMemoryDirectory = <SETTING_PATH>, preserve other keys, write 2-space indent (create .claude/ if absent).
- Confirm:
→ updated .claude/settings.local.json.
If the finding reported a DIFFERENT existing value, first prompt:
autoMemoryDirectory currently points to <existing-value>. Overwrite with <SETTING_PATH>? (y/n).
Step 6 — Advisory rules: 4 & 5 (NO writes)
For each ✗ in Rule 4 (worktree) and Rule 5 (plugin/MCP relevance), print the finding
detail and its recommendation. Offer NO fix and write NOTHING — guidance for the
user to apply manually. Rule 5 is a judgment call; present it as "review", not a
verdict.
For wt1 hardcoded-port findings that carry a references array, print each
path:line indented under the finding so the user sees every place the port leaks:
✗ wt1 compose: hardcoded host port 5432:5432 on service "db"
→ drop host side → "5432"; reach it via `docker compose port db 5432`
references (5432):
docker-compose.yml:7
.env:3
src/db.py:12
README.md:40 (likely unrelated)
Step 7 — Final summary
Summary: <N> fixes applied, <M> declined, <K> skipped. Rule 4 advisory: <P> findings (no changes made).