Skip to content

writ System scope: TargetSystem returns / on Windows, and adopt classifies against %SystemRoot% #761

Open

Description

@David-Noble-at-work

Feature: #762 — The writ lifecycle surface: reconcile, scopes, and the vocabulary settlement

On Windows, three parts of writ disagree about where the System scope points. On Unix two of them agree by
accident and the third is unreachable.

Ruled 2026-08-31

System targets / on Unix and %SystemDrive%\ on Windows. Period.

What the code does

Site Windows root Unix root
cmd/writ/writ/layer.go:55 TargetSystem() / — no Windows branch exists /
cmd/writ/writ/adopt/batch.go:274 inferScope %SystemRoot% — that is C:\Windows /
the ruling %SystemDrive%\ — that is C:\ /

TargetSystem():

func TargetSystem() string {
	if configured := viper.GetString("writ.targets.system"); configured != "" {
		return expandPath(configured)
	}
	return "/"
}

There is no runtime.GOOS branch. On Windows a bare / is drive-relative — it anchors to whichever drive
the process happens to be standing on, which is the exact defect OpenTree's absolute-path requirement
exists to prevent (see SetStoreRoot, which absolutizes for the same reason).

inferScope's doc comment:

Unix: paths under $HOME are Home, paths under / are System. Windows: paths under %USERPROFILE% are
Home, paths under %SystemRoot% are System.

%SystemRoot% is C:\Windows. %SystemDrive%\ is C:\. So on Windows, writ adopt classifies a file as
System only if it sits under C:\Windows, while writ deploy would write System files to a drive-relative
/. A file adopted into System/ cannot be deployed back to where it came from, and a file under
C:\ProgramData is classified Home or not at all.

Why it has not been noticed

Unix has no %SystemDrive%, and / is correct there, so both sites agree on the platform the suite is
developed on. The divergence exists only on Windows, and writ deploy to System on Windows is not
something the scenario tests exercise.

Fix

TargetSystem() branches on runtime.GOOS: %SystemDrive%\ on Windows, / elsewhere, with the
writ.targets.system override winning on both as it does today. inferScope classifies against the same
function rather than against its own idea of the root -- the duplication is what let them drift.

Verification

  • On Windows, TargetSystem() returns %SystemDrive%\ and never a drive-relative path.
  • writ adopt classifies C:\ProgramData\x and C:\Windows\y alike as System, because both are under
    %SystemDrive%\.
  • A file adopted into System/ deploys back to the path it was adopted from -- the round trip that is
    broken today.
  • inferScope and TargetSystem cannot disagree, because only one of them decides.
  • The Unix behaviour is unchanged.

Neighbours

  • The writ.targets.* config key is in the shipped defaults file and absent from the schema, the same shape
    as #746. It is renamed to writ.scopes.* by the
    vocabulary ruling, which is tracked separately.
  • TargetSystem and TargetHome are renamed ScopeSystem / ScopeHome by that same ruling. This issue is
    about the value they return, not their names, and is independently fixable.

Activity

  1. added
    bugSomething isn't working
    Area:CLICommand-line interface
    taskImplementation work item
    Epic:WritDeploymentwrit deployment: layers, adoption, and drift
    on Sep 1, 2026
  2. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Lane 7 of #916 (#926) fixed System's root; adopt's half is open

    Phase 2 of lane 7 (#926, 8b453dc on feature/925-scope-not-target) rebuilt the scope model in cmd/writ/writ/layer.go: ScopeSystem(), formerly TargetSystem(), is %SystemDrive%\ on Windows and / elsewhere, and a writ.scopes key named for it relocates it. That settles the table's first row, as ruled above.

    Adopt's half remains, and the table's second row is not quite what the code does. %SystemRoot% appears only in inferScope's doc comment (cmd/writ/writ/adopt/batch.go) and in writ adopt's help (cmd/writ/writ/adopt_cmd.go); the code makes everything outside the home directory System, on every platform, so a file under C:\ProgramData is classified System, not Home. And collectItem takes System's root to be / itself rather than from the model: on Windows, the drive-relative root this issue describes.

    Lane 7's plan (docs/plans/task/926-scope-flag.md, open question 6, not yet ruled) proposes that adopt infer the scope whose root is the deepest that holds the item, over ScopeOrder(), and take that scope's root from the model; and that adopt record its scope in lower case, as deploy does. #931's item 4 had handed that inference to lane 7.

  3. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Joins PR C as lane 32 of #916; lane 7's phase 4 closes it

    Ruled 2026-10-02 (open question 6 of lane 7's plan, #926): adopt infers each item's scope from the scope model, as the scope whose root is the deepest that holds the item, and takes that scope's root from the model. On Windows, System is then %SystemDrive%\, and a file under C:\ProgramData is adopted into ProgramData. The run records its scope in lower case, as deploy's runs do. With lane 7's phase 2, which fixed ScopeSystem(), that closes this issue when PR C merges.

  4. added 2 commits that reference this issue on Oct 2, 2026
    0440bca
    e2a75b2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Area:CLICommand-line interfaceEpic:WritDeploymentwrit deployment: layers, adoption, and driftbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions