Skip to content

T-MYWI-preflight-probe-worktree-live-grants

State: open/ready · Impact: medium · Complexity: small

AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-09-27 because the task is autonomy: autonomous/pr. Review the Goal, Approach, Today, Areas, and Acceptance-criteria carefully before trusting it.

A gap in the sdlc plugin’s own /sdlc:task-work tooling, surfaced by the post-mortem of T-IEAL-config-path-globs-survive-moves in git@github.com:sksizer/dev.git, needs closing:

preflight_permissions.ts (Step 3b) reported hard gaps for bun, cargo, Write, and Edit under the new worktree path; all were false positives — the harness’s actual permission grants weren’t reflected in the settings files the probe reads. Not blocking, but the probe and the live sandbox disagreeing is worth a look.

This is a recurrence of a previously-tracked family: T-0AM0-preflight-probe-honors-runtime-edit-grant was “the designated tracker for the Step 3b false-positive-gap family” (it had already absorbed several duplicate reports), but it — along with T-DTAQ-preflight-probe-auto-mode-and-abs-globs — is now closed/superseded by D-VSLI-distributed-work-runner-architecture, on the premise that the work-runner engine will absorb /sdlc:task-work’s step sequencing at cutover. That cutover has not happened yet: preflight_permissions.ts is still the live Step 3b probe, it still reads static settings files rather than the runtime’s actual grants, and it just produced a fresh set of false positives. T-WOL2-task-work-preflight-probes-file-mutation-tools (closed/done, #290) previously fixed a related but distinct bug (Write/Edit weren’t probed at all); this is a different failure mode (the probe runs, but disagrees with what’s actually granted).

preflight-permissions.ts derives every gap purely from static files, with no path that reads the harness’s actual live grants:

LocationRole today
preflight-permissions.ts#BLANKET_EDIT_MODES (around line 152)The only file-mutation blanket-allow signal: defaultMode equal to acceptEdits or bypassPermissions, read from the same three settings files. Any other runtime mode (or a mode granted by the harness but never written to a settings file at all) is invisible to the probe.
preflight-permissions.ts#collectPermissions / resolveSettingsPathsReads exactly ~/.claude/settings.json, <repo-root>/.claude/settings.json, and <repo-root>/.claude/settings.local.json. A grant the operator holds through any other channel — an interactive session grant not yet flushed to settings.local.json, a worktree-specific override, or a mode set only for the running process — never appears in these three files.
preflight-permissions.ts#main, Tier-1 loop (around lines 582–586)Emits <pm>: missing Bash(<pm>:*) for every project-resolved package manager (resolveProjectManagers) whose verb isn’t matched in the static allow set — with no fallback check of whether the verb actually runs.
preflight-permissions.ts#main, file-mutation loop (around lines 612–620)Emits <tool>: missing <tool>(<worktree-glob>) for Write/Edit whenever allowsTool returns false against the static settings — with no fallback check of whether the path is actually writable.

This is a recurrence, not a new bug: three prior tasks in the same family (T-0AM0-preflight-probe-honors-runtime-edit-grant, T-OHMA-preflight-permissions-advisory-tiebreaker, T-0X9M-probe-credits-session-observed-tools) converged on the same diagnosis and the same class of fix — “treat a failing static probe as advisory and add an empirical write/exec tiebreaker” — but none of it shipped before all three were closed (closed/superseded on T-0AM0, closed/superseded/folded on the others) under the premise that D-VSLI-distributed-work-runner-architecture’s work-runner engine would absorb /sdlc:task-work’s Step 3b entirely at cutover. That cutover has not happened: preflight-permissions.ts is unchanged from what those tasks describe, and this task’s own trigger — a worktree run where bun, cargo, Write, and Edit all worked at runtime but were reported as hard gaps — is that same failure mode firing again.

There is no existing “live grants” signal in this codebase to reconcile against instead: a repo-wide search for a permission-mode environment variable, a runtime introspection call, or any prior partial implementation of the empirical-tiebreaker idea (grep-checked against CLAUDE_PERMISSION_MODE, permission-mode, effective permission, session-observed) turns up nothing. The three prior tasks proposed the tiebreaker; none built it.

Add an empirical fallback tier that runs only when the static settings scan is about to emit a hard gap, and let its result override a static false-positive — reconciling the probe against what the harness will actually let the implementer do, not just what a settings file says:

  • Package-manager verbs (Tier 1: bun, cargo, and the rest of resolveProjectManagers’s resolved set). Before pushing a <pm>: missing Bash(<pm>:*) gap, attempt a cheap, side-effect-free invocation of that manager (a --version-shaped call) through the command seam. A successful run means the harness already grants the verb at runtime regardless of what the settings files say; demote the gap to an advisory warning: line on stderr instead of the hard stdout gap, so a stale settings file stays visible to the operator without blocking the run.
  • Write/Edit on the worktree path. Before pushing a <tool>: missing <tool>(<worktree-glob>) gap, empirically test writability of the nearest existing ancestor of the worktree path (the worktree itself doesn’t exist yet at Step 3b) with a throwaway mkdtemp-and-remove. A successful write means the tool is granted at runtime; demote the gap to an advisory warning: line on the same terms as above.
  • The static scan stays the fast path: when it already grants the permission, skip the empirical probe entirely, so the common case pays no extra cost.
  • The empirical probe is strictly best-effort: any failure, timeout, or exception from the probe itself falls back to today’s static-scan verdict unchanged — the new tier can only demote a false-positive hard gap to an advisory warning, never suppress a real gap outright or introduce a new false negative.
  • The probe never runs against an explicit deny rule. Probing tests OS-level capability (can this process exec the verb / write the path), not the harness’s own tool-call permission gate — a subprocess spawned from inside an already-approved script inherits that process’s OS privileges rather than being separately intercepted, so it will often succeed regardless of what the harness would grant for a fresh, independent tool call. That’s an acceptable proxy for “is this simply unmentioned by any settings file” (the session-grant-not-yet-flushed case this task targets), but it must never be used to override a deliberate deny: an operator who explicitly denies a verb or path has stated intent that OS-level capability cannot override.
  1. Before writing the probe helpers, run an end-to-end validation spike: add an explicit deny rule for a Bash verb (e.g. deny: ["Bash(bun:*)"] in a scratch settings.local.json) and, from inside a script the harness has already approved to run (the same shape preflight-permissions.ts itself runs under), attempt to exec that verb through the command seam. This settles whether OS-level process execution is gated by the harness’s own deny rules or is a strictly looser superset of them. If it’s a strict superset (execution succeeds despite the deny) — the likely outcome, since a subprocess spawned from an already-approved Bash call inherits that call’s OS-level privileges rather than being separately intercepted — the empirical probe can only ever be evidence for “the OS permits this,” never for “the harness’s tool-call gate would grant this.” That’s fine for this task’s actual target (a verb/path the settings files simply don’t mention, e.g. a session grant not yet flushed to disk), but it means the probe must never run at all when the static gap comes from an explicit deny rule — see step 2/3’s guard. Record the spike’s finding in the PR description; if OS-level and harness-level permission turn out to be enforced by the same mechanism (i.e. the deny actually blocks the probe too), the guard in steps 2/3 is still correct, just redundant.
  2. In preflight-permissions.ts, add two exported empirical-probe helpers:
    • probeBashVerbRuns(verb: string, cwd: string): Promise<boolean> — runs a trivial, side-effect-free invocation of verb (e.g. --version) through the command seam (spawnRunnerAsync/runCheckedAsync in solutions/ontological/lib/util/command.ts), returning true iff it exits zero, false on any non-zero exit, spawn failure, or missing binary.
    • probeDirWritable(dir: string): boolean — synchronously creates and removes a throwaway temp file/dir under dir (the nearest existing ancestor of the target path, walking up via dirname until one exists), returning true iff the write+remove round-trips cleanly.
  3. In main’s Tier-1 loop, when !perms.allowsVerb(pm) would push a gap, first check deniesVerbFamily(perms.deny, pm) (already in this file). If the verb is explicitly denied, push the hard gap exactly as today — never run the empirical probe against an explicit deny, regardless of what the OS would permit; respecting a deliberate deny is a matter of operator intent, not technical capability. Only when the gap comes from an absent grant (no matching allow, and no family-wide deny) call probeBashVerbRuns(pm, repoRoot); on true, move the line from gaps to warnings (prefixed to distinguish “static settings disagree with the empirical result” from the existing PM-family body-text warnings), so the exit code no longer reflects it. On false or a thrown error, push the gap exactly as today.
  4. In main’s file-mutation loop, when !perms.allowsTool(tool, worktreePath) would push a gap, first check matchesTool(perms.deny, tool, worktreePath) (already in this file). If it matches an explicit deny, push the hard gap exactly as today — same guard and same rationale as step 2. Only when the gap comes from an absent grant, call probeDirWritable on the nearest existing ancestor of worktreePath first; on true, apply the same gap-to-warning demotion as step 2. On false or a thrown error, push the gap exactly as today.
  5. Update the module doc-comment (the “Package-manager permissions use a TWO-TIER model” block and the “Settings paths probed” list) to document the new empirical fallback as an additional tier that demotes a cleared hard gap to an advisory warning.
  6. Update task-work.md Step 3b to describe the new fallback: a gap the empirical probe clears surfaces only as an advisory warning on stderr, not a hard gap, so the AskUserQuestion block fires only for gaps the empirical probe could not clear.
  7. Extend preflight-permissions.test.ts with cases covering: a resolved PM with no settings grant (absent, not denied) but a successful empirical probe (gap demoted to an advisory warning, exit 0); a Write/Edit case where the settings omit the tool but the ancestor directory is actually writable (same demotion); a case where the empirical probe genuinely fails too (existing hard-gap behavior unchanged, exit 1); and — per AC-4 — a case where the settings explicitly deny the verb/tool and the empirical probe would (if run) succeed, asserting the gap still surfaces as a hard failure because the deny guard skips the probe entirely.
  8. Run bun test against solutions/ontological/lib/model/entities/task/ops/preflight-permissions.test.ts and fix any regressions before committing.
AreaNote
solutions/ontological/lib/model/entities/task/ops/preflight-permissions.tsThe probe itself: BLANKET_EDIT_MODES, the Tier-1 PM loop, the file-mutation loop, and the new empirical-probe helpers.
solutions/ontological/lib/model/entities/task/ops/preflight-permissions.test.tsNew coverage for the empirical-probe-succeeds / empirical-probe-fails paths, for both Bash verbs and Write/Edit.
solutions/ontological/plugin/plugins/sdlc/skills/task-work/task-work.md (Step 3b)Doc update describing the new fallback tier and its effect on which gaps still trigger the AskUserQuestion block.
solutions/ontological/lib/util/command.tsThe command seam the new Bash empirical probe must shell through (spawnRunnerAsync/runCheckedAsync), per this repo’s shell-out convention.
  • AC-1: preflight-permissions.ts empirically probes a resolved package-manager Bash verb (e.g. bun, cargo) before emitting its Tier-1 hard gap, and a successful probe demotes that gap from stdout (exit 1) to an advisory warning: line on stderr, even when no settings file grants Bash(<pm>:*).
  • AC-2: preflight-permissions.ts empirically probes writability of the worktree path’s nearest existing ancestor before emitting a Write/Edit hard gap, and a successful probe demotes that gap from stdout (exit 1) to an advisory warning: line on stderr, even when no settings file grants the tool for that path.
  • AC-3: when the empirical probe itself fails, throws, or times out, the probe’s exit code and gap output are byte-identical to today’s behavior — no new false negative is introduced.
  • AC-4: a verb or path matched by an explicit deny rule always stays a hard gap and is never passed to the empirical probe, regardless of what the probe would report — verified by a test case with a deny rule that would (if probed) empirically succeed, asserting the gap still surfaces on stdout with exit 1.
  • AC-5: bun test against solutions/ontological/lib/model/entities/task/ops/preflight-permissions.test.ts passes, including the AC-4 deny-guard case and new cases exercising the empirical-probe-succeeds and empirical-probe-fails paths for both a Bash verb and Write/Edit.
  • AC-6: solutions/ontological/plugin/plugins/sdlc/skills/task-work/task-work.md Step 3b documents the new empirical-fallback tier and states which gaps still trigger the operator-facing AskUserQuestion block afterward.
  • AC-7: the Approach step 0 validation spike ran, and its finding (does OS-level execution from inside an already-approved process reflect the harness’s own deny rules, or is it a strict superset) is recorded in the PR description.
  • Reading Claude Code’s internal runtime permission state through a direct introspection API — this task’s Today section confirms no such signal exists in this codebase today; the empirical probe is the mechanism, not a live query.
  • The D-VSLI-distributed-work-runner-architecture work-runner engine cutover that will eventually retire /sdlc:task-work’s own step sequencing (and this probe along with it).
  • Crediting a tool family merely because it ran earlier in the same session (the framing T-0X9M-probe-credits-session-observed-tools proposed and folded away) — this task’s probe re-checks fresh each run rather than tracking session history.
  • Auto-rewriting the operator’s settings files; the probe still only reports, never edits settings.json/settings.local.json.
  • Extending the probe to skill-internal scripts. T-NUML-task-work-preflight-permissions-probe-extension-for-skill-internal-scripts previously proposed tracking this, but it’s closed/superseded under the same D-VSLI premise this task argues hasn’t actually landed — so the gap is currently untracked, not owned by a live task.
  • Near-miss glob-form detection (single-slash vs. double-slash absolute paths) — that is T-DTAQ-preflight-probe-auto-mode-and-abs-globs’s concern, already closed/superseded under the same D-VSLI premise; this task does not reopen it.
  • none

Spawned by /sdlc:spawn-task-pr on 2026-09-27 UTC from T-IEAL-config-path-globs-survive-moves in git@github.com:sksizer/dev.git.


← Back to Tasks