Skip to content

fix(execution): refuse a relative configured command - #342

Merged
slidict-ai-team[bot] merged 2 commits into
mainfrom
fix/338-reject-relative-configured-command
Nov 14, 2026
Merged

slidict-ai-team[bot] merged 2 commits into
mainfrom
fix/338-reject-relative-configured-command

Conversation

@slidict-ai-team

@slidict-ai-team slidict-ai-team Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Closes #338.

CommandResolver.Resolve turned a configured relative command into an absolute path by resolving it against the process's current directory, so a wip.yml committed to a repository someone had just cloned could point wip at a binary inside that repository and have it run with their privileges. #329 closed the launcher side; this path defeated that guard rather than tripping it, because the resolver had already absolutized the value before the guard saw it.

The rule

A configured wslc.command / compose.command must be a bare name, searched on PATH, or a fully qualified path. Anything else is refused before any resolver sees it.

This is what the setting is already documented to do — the wslc: block lists auto, "any other name: must be executable on PATH", and "an absolute path". A relative path was never documented, so this brings the code to the page rather than changing documented behaviour.

Three details:

  • The guard sits ahead of resolveExecutable, so an injected resolver cannot route around it. Covered by a test.
  • Path.IsPathFullyQualified, not IsPathRooted. IsPathRooted accepts the drive-relative C:wslc.exe and the root-relative \wslc.exe, which still depend on the current drive or directory. IsPathRooted is used only to detect that a separator-less value like C:wslc.exe is a path at all — as a bare name it would otherwise reach the PATH walk and come back as itself.
  • CommandNotFoundException is reused, keeping it the only failure callers have to handle, as that file's contract states. The message carries the meaning: the value, the rule, and why.

The candidates loop is untouched. Candidates are code-supplied (DefaultCandidates, ["git"] in RulesFetcher), never wip.yml content.

Tests

ExplicitRelativePathIsFrozenAsAbsolutePath asserted the behaviour being removed, so its setup became the refusal test. Added: the absolute path still resolving, a bare name still resolving on PATH, the drive- and root-relative forms, and the injected-resolver case. UnnormalizableCandidate… now uses an absolute path so it still exercises normalization rather than the new guard.

Each refusal test puts a real file where the value points, so only the guard can make it throw — not an absent file.

Verification

On the Windows host, SDK 10.0.403:

  • dotnet restore wip.slnx --locked-mode — up to date, no lock file changed
  • dotnet build wip.slnx --configuration Release — 0 warnings, 0 errors
  • dotnet test — 1746 total, 0 failed, 1651 passed, 95 skipped
  • CommandResolverTests alone — 16 total, 0 failed, 0 skipped, so the Windows-only cases really ran

Mutation check: with the RejectRelativePath call commented out, 7 cases in that class fail (the refusal test, all four drive/root-relative forms, and both injected-resolver forms). The tests fail without the fix.

The same change was also run on Linux during implementation: 1746 total, 0 failed, 10 skipped — there the four Windows-only cases skip, which is why they were re-run here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LhR19gpMx4ZBuq6pkP4fgY

Summary by CodeRabbit

  • Bug Fixes
    • Configured commands using relative or drive-relative paths are now rejected, including root-relative paths.
    • On Windows, network and device paths are also rejected; absolute paths rooted on a local drive remain supported.
    • Bare command names can still be resolved through PATH.

wslc.command and compose.command come from the project's wip.yml. A
relative path was resolved against the directory wip ran from, so a
cloned repository could point wip at a binary inside itself. Accept only
a bare name, searched on PATH, or a fully qualified path; drive- and
root-relative forms are refused too.

Closes #338

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@slidict-ai-team
slidict-ai-team Bot enabled auto-merge November 14, 2026 13:43
@coderabbitai

coderabbitai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

CommandResolver.Resolve rejects configured relative paths before calling the executable resolver. On Windows, it also rejects rooted paths that are not drive-rooted. Tests cover rejected path forms, accepted absolute paths and bare names, and normalization failure.

Changes: Configured Command Path Validation

Layer / File(s) Summary
Validate configured commands
src/Wip.Core/Execution/CommandResolver.cs, tests/Wip.Tests/CommandResolverTests.cs
Resolve rejects configured relative paths before invoking the resolver and rejects non-local rooted paths on Windows. Tests cover error messages, injected resolver behavior, accepted absolute paths and bare names, platform-specific paths, and normalization failure.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: abechan1, claude

Merge Risk: Minimal · up to 498cd

This change makes configured wslc.command and compose.command values refuse relative paths and non-local Windows paths, so a repository's wip.yml can no longer select a binary relative to the current directory. Bare names on PATH and fully qualified paths still work. No concrete defect was identified, and the change appears ready to merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "fix(execution): refuse a relative configured command" matches the main change. CommandResolver.Resolve now refuses configured relative, drive-relative, and root-relative command values befo…
Linked Issues check Passed Issue #338 (direct) requires that a relative configured command cannot make wip run a binary resolved against the current directory. At head, CommandResolver.Resolve calls RejectRelativePath(configure…
Out of Scope Changes check Passed The change set at head consists of the guards and helpers in src/Wip.Core/Execution/CommandResolver.cs (RejectRelativePath, RejectNonLocalPath, IsDriveRoot) and changes in tests/Wip.Tests/CommandResol…
Full details: Title check

Explanation

The title "fix(execution): refuse a relative configured command" matches the main change. CommandResolver.Resolve now refuses configured relative, drive-relative, and root-relative command values before the executable resolver runs, and the tests cover these refusals. The title is specific and short, and it names the affected area (execution) and the behavior change (refusing a relative configured command). It does not mention the Windows UNC and device path rejection, but that is a secondary detail and the title does not need to cover it.

Full details: Linked Issues check

Explanation

Issue #338 (direct) requires that a relative configured command cannot make wip run a binary resolved against the current directory. At head, CommandResolver.Resolve calls RejectRelativePath(configured) and then RejectNonLocalPath(configured) before resolveExecutable for any value other than "auto". RejectRelativePath treats a value as a path when it contains a separator or is rooted, and refuses any path value that is not Path.IsPathFullyQualified. This covers the relative, drive-relative (C:wslc.exe) and root-relative (\wslc.exe) forms named in the issue. Bare names still go through ResolveExecutable, whose PATH walk skips non-fully-qualified PATH entries. Both guards run before the injected resolver, so an injected resolver cannot bypass them. The chosen rule is the refusal option from the issue. The head also refuses Windows UNC and device paths (RejectNonLocalPath), which the change since the previous review adds. This is consistent with the same threat model and with the PR objectives. Mapped drive roots such as Z:\ remain allowed through IsDriveRoot. Test contents were taken from the PR summary and the test summary; I did not read tests/Wip.Tests/CommandResolverTests.cs. The author reports the locked restore, Release build and full test run; I did not re-execute them. The documentation criterion is not a coding requirement, and I did not read the README text the PR cites.

Full details: Out of Scope Changes check

Explanation

The change set at head consists of the guards and helpers in src/Wip.Core/Execution/CommandResolver.cs (RejectRelativePath, RejectNonLocalPath, IsDriveRoot) and changes in tests/Wip.Tests/CommandResolverTests.cs. RejectNonLocalPath is a new addition since the previous review. It is connected to #338 because a UNC or device path in wip.yml also selects a binary outside the user's trusted local locations, so it is the same class of hazard. It is not a demonstrated unrelated change. The candidates loop and ResolveFile logic are unchanged and used only for code-supplied candidates. The PR description does not mention the UNC and device refusal, but the PR objectives do. This is a documentation gap in the description, not an out-of-scope code change. I did not read the test diff, so test-level scope is based on the summary.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

  • Source mode: Auto
  • Repository: slidict/wip
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: b363e05
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: bbfe85fb-a594-48e2-a8c2-05fa451b58ab
Commits

Reviewing files that changed from the base of the PR and between fc48549 and 498cdf9.

Files selected for processing: 2

  • src/Wip.Core/Execution/CommandResolver.cs
  • tests/Wip.Tests/CommandResolverTests.cs

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Nov 14, 2026
IsPathFullyQualified accepts \\server\share\wslc.exe, so a wip.yml could
run a binary over SMB without knowing where it was cloned. On Windows a
configured path must now have a drive root; a mapped drive stays allowed.

Also reword the relative-path refusal so the "compose command" label
no longer prints "command command".

Refs #338

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@slidict-ai-team
slidict-ai-team Bot merged commit 55938dd into main Nov 14, 2026
5 checks passed
@slidict-ai-team
slidict-ai-team Bot deleted the fix/338-reject-relative-configured-command branch November 14, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A configured relative command is resolved against the current directory, so a cloned wip.yml can choose the binary wip runs

1 participant