Skip to content

T-Z8VC-task-work-preflight-permissions-probe

Status: closed/done · Impact: medium · Complexity: small

Auto-generated from a /sdlc:task-work post-mortem. Review and promote to open/ready before picking up.

The current /sdlc:task-work flow does not check that the operator’s sandbox permissions cover the tool families the task body will need (node/npm, cargo, uv, pnpm, etc.). When a task implies one of these tools and the permission isn’t granted, the implementing sub-agent discovers the gap mid-Step 6, has to BLOCK with <blocked>, wait for the user to grant the permission, then resume. Adding a pre-flight probe in Step 3 or Step 4 closes this loop before any worktree is created.

Cited by T-6D5U-add-astro-docs-site — that task’s implementation hit exactly this BLOCK/unblock cycle because the Bash(node:*), Bash(npm:*), Bash(npx:*), Bash(pnpm:*), Bash(yarn:*) permissions were not in the allowlist when the task was picked up.

/sdlc:task-work Step 3 (“Pre-flight on main”) verifies the working tree is clean. Step 4 (“Create the worktree”) branches and inits. Neither step inspects the task body for tool-family hints, and neither step checks the current sandbox’s permission set.

The detection-after-the-fact path works (the sub-agent commits a <blocked> section, the user grants permission, a follow-up /sdlc:task-work run unblocks and resumes) but it produces two paired commits (block + unblock) that net to no change, and it costs a context-switch the user does not need.

Step 3 of /sdlc:task-work adds a pre-flight probe:

  1. Scan the task body for tool-family signals — a small regex/keyword table over npm , node , cargo, uv run, pnpm, yarn, pytest, etc.
  2. For each signal hit, check whether the corresponding Bash(<cmd>:*) permission is present in the resolved settings (project + user + project-local merge).
  3. If anything is missing, surface to the user via AskUserQuestion before creating the worktree: “This task references npm / node / etc. but your sandbox lacks Bash(npm:*). Grant permission via /config, or proceed knowing the implementer will hit a sandbox denial?”

The probe is best-effort — false negatives are acceptable; what matters is catching the obvious cases.

  1. Implement plugin/skills/task-work/preflight_permissions.py. Inputs: task file path (positional). Settings paths discovered automatically (~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json). The script (a) scans the task body for tool-family signals against a small hard-coded table, (b) reads + merges the three settings files honoring Claude Code’s deny-overrides-allow precedence, (c) prints the missing-permission list to stdout in tool-family: missing-permission form, one per line. Exits 0 if no gaps, 1 if gaps surfaced.
  2. In plugin/skills/task-work/SKILL.md, add Step 3a (sub-step of Step 3) that shells out to the helper. On exit 1, the prose says “surface the gaps to the user via AskUserQuestion; options: grant via /config / proceed anyway / abort”. On exit 0, the step is silent.
  3. Seed the signal table with the four tool families seen in recent post-mortems: npm / node / pnpm / yarn (the astro-docs incident), plus uv, cargo, pytest. Document the table in the helper’s docstring so future contributors can extend it.
  • plugin/skills/task-work/SKILL.md — add Step 3a (permission probe) as a sub-step of the existing Step 3 “Pre-flight on main”.
  • plugin/skills/task-work/preflight_permissions.py (new) — the probe helper. Reads ~/.claude/settings.json, .claude/settings.json, and .claude/settings.local.json; merges permissions.allow and permissions.deny per Claude Code’s precedence (local > project > user, with deny overriding allow); returns the missing-permission list. The helper’s purpose lives in its module docstring — no separate README needed.
  • plugin/skills/task-work/invariants.yaml — pin a required phrase that names Step 3a, so future skill drift trips the lint.
  • AC-1: Running /sdlc:task-work <task> on a task whose body references npm --prefix site build, with Bash(npm:*) missing from the sandbox, surfaces the missing permission to the user before the worktree is created.
  • AC-2: Running on a task that references no special tool family is a no-op — the probe passes silently.
  • AC-3: The probe ships with a small, documented signal table that later contributors can extend.
  • Auto-granting permissions. The probe surfaces gaps; the user grants.
  • Detecting permissions for non-Bash tool families (MCP servers, file permissions, etc.).
  • none

Spawned by /sdlc:task-work post-mortem of T-6D5U-add-astro-docs-site on 2026-05-20.

Captured by /sdlc:task-work on 2026-05-21. PR: pending.

  • AC-1: auto — test_preflight_permissions.py :: case_ac1_npm_missing_permission_surfaces fixtures a task referencing npm --prefix site build with no Bash(npm:*) grant, runs the helper, asserts exit 1 and the npm: missing Bash(npm:*) line on stdout. A live smoke against the originating task file with the operator’s real ~/.claude/settings.json reproduced the gap list (npm/node/yarn/uv/cargo/pytest).
  • AC-2: auto — case_ac2_no_signals_is_silent_noop fixtures a task body with no tool-family hints, asserts exit 0 and empty stdout regardless of settings contents. case_frontmatter_only_does_not_match_signals_in_yaml_values pins the corollary: YAML-side tags don’t leak into body detection.
  • AC-3: auto — case_ac3_table_is_extendable introspects SIGNAL_TABLE, verifies each entry has {family, verb, hints}, and confirms the seven seed families documented in the module docstring are present. The table is the documented extension point — future contributors append a dict, no other code path changes.
  • The Python 3.14 dataclass-loading-via-importlib gotcha (sys.modules[cls.__module__] is None when the module isn’t registered before exec_module) was caught on the first test run and fixed in two minutes — the unit-test scaffolding pays for itself the moment something surprises you.
  • The signal table being a flat list-of-dicts (rather than an abstract Signal class) makes the “is this extendable” AC trivial to verify in a test, AND trivial for a future contributor to understand without reading a class hierarchy.
  • Quality checks (run_quality_checks.py) hit 7/7 on the first invocation after the implementation landed — the helper has zero cross-skill dependencies, so nothing downstream had to move.
  • The prior /sdlc:task-work run for this very task bailed after ensure-ready’s READY: marker instead of running Step 5b onwards. The current run had to resume by manually inspecting worktree/branch state, replaying start_task.py, and resolving a rebase conflict that arose because the start commit and the verify commit both touched the same frontmatter — the verify commit had stamped a fresher readiness_verified_at than start_task.py rebased onto. Automation gap: start_task.py should read the existing readiness_verified_at off the worktree’s task file (post-ensure-ready) and preserve it when rewriting main’s task file, so the rebase is a clean fast-forward instead of a content-merge of the same frontmatter from two vantage points. → T-N2X3-start-task-preserves-readiness-stamp
  • The signal table’s hint list is hand-tuned ("npm ", "npm install", "`npm`", etc.) which is fine for v1 but fragile across task authors’ phrasings. Automation gap: a follow-up run could compile the hint list from real task-file shell-out occurrences via command grep rather than maintaining a hand-tuned table. Out of scope for this task; worth noting in the helper docstring as future work.

← Back to Tasks