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 forbun,cargo,Write, andEditunder 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:
| Location | Role 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 / resolveSettingsPaths | Reads 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.
Proposed
Section titled “Proposed”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 ofresolveProjectManagers’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 advisorywarning:line on stderr instead of the hard stdout gap, so a stale settings file stays visible to the operator without blocking the run. Write/Editon 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 advisorywarning: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
denyrule. 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.
Approach
Section titled “Approach”- Before writing the probe helpers, run an end-to-end validation spike: add
an explicit
denyrule for a Bash verb (e.g.deny: ["Bash(bun:*)"]in a scratchsettings.local.json) and, from inside a script the harness has already approved to run (the same shapepreflight-permissions.tsitself 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 explicitdenyrule — 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. - In
preflight-permissions.ts, add two exported empirical-probe helpers:probeBashVerbRuns(verb: string, cwd: string): Promise<boolean>— runs a trivial, side-effect-free invocation ofverb(e.g.--version) through the command seam (spawnRunnerAsync/runCheckedAsyncinsolutions/ontological/lib/util/command.ts), returningtrueiff it exits zero,falseon any non-zero exit, spawn failure, or missing binary.probeDirWritable(dir: string): boolean— synchronously creates and removes a throwaway temp file/dir underdir(the nearest existing ancestor of the target path, walking up viadirnameuntil one exists), returningtrueiff the write+remove round-trips cleanly.
- In
main’s Tier-1 loop, when!perms.allowsVerb(pm)would push a gap, first checkdeniesVerbFamily(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) callprobeBashVerbRuns(pm, repoRoot); ontrue, move the line fromgapstowarnings(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. Onfalseor a thrown error, push the gap exactly as today. - In
main’s file-mutation loop, when!perms.allowsTool(tool, worktreePath)would push a gap, first checkmatchesTool(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, callprobeDirWritableon the nearest existing ancestor ofworktreePathfirst; ontrue, apply the same gap-to-warning demotion as step 2. Onfalseor a thrown error, push the gap exactly as today. - 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.
- Update
task-work.mdStep 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 theAskUserQuestionblock fires only for gaps the empirical probe could not clear. - Extend
preflight-permissions.test.tswith 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. - Run
bun testagainstsolutions/ontological/lib/model/entities/task/ops/preflight-permissions.test.tsand fix any regressions before committing.
| Area | Note |
|---|---|
solutions/ontological/lib/model/entities/task/ops/preflight-permissions.ts | The 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.ts | New 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.ts | The command seam the new Bash empirical probe must shell through (spawnRunnerAsync/runCheckedAsync), per this repo’s shell-out convention. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
preflight-permissions.tsempirically 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 advisorywarning:line on stderr, even when no settings file grantsBash(<pm>:*). - AC-2:
preflight-permissions.tsempirically probes writability of the worktree path’s nearest existing ancestor before emitting aWrite/Edithard gap, and a successful probe demotes that gap from stdout (exit 1) to an advisorywarning: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
denyrule 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 adenyrule that would (if probed) empirically succeed, asserting the gap still surfaces on stdout with exit 1. - AC-5:
bun testagainstsolutions/ontological/lib/model/entities/task/ops/preflight-permissions.test.tspasses, 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 andWrite/Edit. - AC-6:
solutions/ontological/plugin/plugins/sdlc/skills/task-work/task-work.mdStep 3b documents the new empirical-fallback tier and states which gaps still trigger the operator-facingAskUserQuestionblock 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.
Out of scope
Section titled “Out of scope”- 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/supersededunder 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.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”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.