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.
Proposed
Section titled “Proposed”Step 3 of /sdlc:task-work adds a pre-flight probe:
- Scan the task body for tool-family signals — a small regex/keyword
table over
npm,node,cargo,uv run,pnpm,yarn,pytest, etc. - For each signal hit, check whether the corresponding
Bash(<cmd>:*)permission is present in the resolved settings (project + user + project-local merge). - If anything is missing, surface to the user via AskUserQuestion
before creating the worktree: “This task references
npm/node/ etc. but your sandbox lacksBash(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.
Approach
Section titled “Approach”- 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’sdeny-overrides-allowprecedence, (c) prints the missing-permission list to stdout intool-family: missing-permissionform, one per line. Exits 0 if no gaps, 1 if gaps surfaced. - 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. - Seed the signal table with the four tool families seen in
recent post-mortems:
npm/node/pnpm/yarn(the astro-docs incident), plusuv,cargo,pytest. Document the table in the helper’s docstring so future contributors can extend it.
Files to touch
Section titled “Files to touch”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; mergespermissions.allowandpermissions.denyper Claude Code’s precedence (local > project > user, withdenyoverridingallow); 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.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Running
/sdlc:task-work <task>on a task whose body referencesnpm --prefix site build, withBash(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.
Out of scope
Section titled “Out of scope”- Auto-granting permissions. The probe surfaces gaps; the user grants.
- Detecting permissions for non-Bash tool families (MCP servers, file permissions, etc.).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-6D5U-add-astro-docs-site on 2026-05-20.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
test_preflight_permissions.py :: case_ac1_npm_missing_permission_surfacesfixtures a task referencingnpm --prefix site buildwith noBash(npm:*)grant, runs the helper, asserts exit 1 and thenpm: missing Bash(npm:*)line on stdout. A live smoke against the originating task file with the operator’s real~/.claude/settings.jsonreproduced the gap list (npm/node/yarn/uv/cargo/pytest). - AC-2: auto —
case_ac2_no_signals_is_silent_noopfixtures 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_valuespins the corollary: YAML-side tags don’t leak into body detection. - AC-3: auto —
case_ac3_table_is_extendableintrospectsSIGNAL_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.
What worked
Section titled “What worked”- The Python 3.14 dataclass-loading-via-importlib gotcha
(
sys.modules[cls.__module__]is None when the module isn’t registered beforeexec_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.
Friction and automation gaps
Section titled “Friction and automation gaps”- The prior
/sdlc:task-workrun for this very task bailed after ensure-ready’sREADY:marker instead of running Step 5b onwards. The current run had to resume by manually inspecting worktree/branch state, replayingstart_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 fresherreadiness_verified_atthanstart_task.pyrebased onto. Automation gap:start_task.pyshould read the existingreadiness_verified_atoff 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 viacommand greprather than maintaining a hand-tuned table. Out of scope for this task; worth noting in the helper docstring as future work.
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-N2X3-start-task-preserves-readiness-stamp — created; closes the rebase-conflict gap surfaced during this run’s resume.