Skip to content

feat(lib): Allow env_add_path and shortcuts field to use variables#6616

Open
abgox wants to merge 5 commits intoScoopInstaller:developfrom
abgox:allow-var-in-field
Open

feat(lib): Allow env_add_path and shortcuts field to use variables#6616
abgox wants to merge 5 commits intoScoopInstaller:developfrom
abgox:allow-var-in-field

Conversation

@abgox
Copy link
Copy Markdown

@abgox abgox commented Mar 11, 2026

Description

  • Allow env_add_path and shortcuts field to use variables.
  • If env_set exists, env_add_path can also use the variables defined by it.

Motivation and Context

Closes #6605

How Has This Been Tested?

Checklist:

  • I have read the Contributing Guide.
  • I have ensured that I am targeting the develop branch.
  • I have updated the documentation accordingly.
  • I have updated the tests accordingly.
  • I have added an entry in the CHANGELOG.

Summary by CodeRabbit

  • New Features
    • Added variable expansion support for environment path operations.
    • Added variable expansion support for shortcut target and icon paths.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

Walkthrough

Adds PowerShell variable expansion to env_add_path, env_rm_path, and shortcuts by using ExpandString; adjusts path construction fallback to previous join/absolute logic when no expansion occurs; and reorders env_add_path to run after env_set in some flows.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Adds Unreleased entry documenting variable support for env_add_path and shortcuts.
Environment Path Management
lib/install.ps1
env_add_path and env_rm_path now ExpandString each entry (use expanded value if changed), otherwise fall back to Join-Path + Get-AbsolutePath + is_in_dir; moved env_add_path to run after env_set in install_app.
Shortcuts Configuration
lib/shortcuts.ps1
Apply ExpandString to shortcut target and icon paths; use expanded strings when different, otherwise retain directory-joined behavior.
Reset Flow
libexec/scoop-reset.ps1
Reordered to call env_set before env_add_path (env_rm_path -> env_rm -> env_set -> env_add_path).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • niheaven

Poem

I nibble tokens, soft and bright,
Expanding paths by moonlit bite,
Dollars unfurl, shortcuts prance,
Paths hop home in a newfound dance,
A cheerful rabbit stamps—advance! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Changes are limited to variable expansion in env_add_path and shortcuts; the ordering change in env_set/env_add_path sequence appears minor but may warrant clarification. Clarify whether the env_set/env_add_path ordering change in install.ps1 and scoop-reset.ps1 is intentional and necessary for variable expansion to function correctly.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding variable support to env_add_path and shortcuts fields.
Linked Issues check ✅ Passed The PR implements variable expansion for env_add_path and shortcuts fields as requested in #6605, using conditional expansion logic and preserving backward compatibility.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/install.ps1 (2)

336-343: Consider extracting duplicated path expansion logic.

Lines 336-343 duplicate the expansion logic from lines 320-327. Consider extracting to a helper function:

♻️ Proposed refactor
+function resolve_env_paths($env_add_path, $dir) {
+    $env_add_path.Where({ $_ }) | ForEach-Object {
+        $path_expand = $ExecutionContext.InvokeCommand.ExpandString($_)
+        if ($path_expand -eq $_) {
+            Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
+        } else {
+            $path_expand
+        }
+    }
+}
+
 function env_add_path($manifest, $dir, $global, $arch) {
     $env_add_path = arch_specific 'env_add_path' $manifest $arch
     $dir = $dir.TrimEnd('\')
     if ($env_add_path) {
         if (get_config USE_ISOLATED_PATH) {
             Add-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global
         }
-        $path = $env_add_path.Where({ $_ }) | ForEach-Object {
-            $path_expand = $ExecutionContext.InvokeCommand.ExpandString($_)
-            if($path_expand -eq $_){
-                Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
-            }else{
-                $path_expand
-            }
-        }
+        $path = resolve_env_paths $env_add_path $dir
         Add-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global -Force
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/install.ps1` around lines 336 - 343, The block that expands PATH entries
is duplicated; extract it into a helper function (e.g., Resolve-PathEntry or
Expand-PathEntry) that accepts a directory ($dir) and a single path entry ($_),
performs $ExecutionContext.InvokeCommand.ExpandString($_), compares the expanded
value to the original, and returns either the expanded string or the result of
Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ } to
preserve current behavior; then replace both duplicated blocks (the loop using
$env_add_path.Where and the earlier occurrence) to call this new helper so the
logic (ExpandString, Join-Path, Get-AbsolutePath, is_in_dir) is centralized.

322-326: Minor: Inconsistent spacing after if.

The new code uses if($path_expand and if($target while the existing codebase typically uses if ($condition with a space.

🧹 Style consistency fix
-            if($path_expand -eq $_){
+            if ($path_expand -eq $_) {
                 Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
-            }else{
+            } else {
                 $path_expand
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/install.ps1` around lines 322 - 326, The if-statements in this snippet
use `if($path_expand` (and elsewhere `if($target`) without the project-standard
space; update these to use a space after `if` (e.g., `if ($path_expand -eq
$_)`), keeping the same condition logic and return values for
Join-Path/Get-AbsolutePath and the else branch, and apply the same spacing
consistency for any other occurrences like `if ($target)` or similar in this
file; do not change variable names `$path_expand`, `$dir`, `$_`, or the
`is_in_dir` call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/install.ps1`:
- Around line 320-327: This block currently bypasses is_in_dir for expanded
entries ($env_add_path -> $path_expand) due to PR `#6616`; decide the intended
behavior and document it: if the bypass is intentional, add a concise code
comment above this block referencing PR `#6616`, explaining why expanded variables
are allowed to expand outside the install dir (the security rationale and any
mitigations), otherwise enforce validation by applying is_in_dir to $path_expand
(e.g., run Get-AbsolutePath/Join-Path then is_in_dir on $path_expand) so
expanded values cannot add arbitrary system paths. Ensure the comment references
$env_add_path, $path_expand, ExpandString, and is_in_dir so future reviewers can
find the logic.

In `@lib/shortcuts.ps1`:
- Around line 5-8: The use of $ExecutionContext.InvokeCommand.ExpandString to
compute $target (and the similar code handling entries at lines 17-20) evaluates
arbitrary PowerShell expressions and is unsafe for untrusted manifests; replace
this with a safe, restricted expansion routine: detect and expand only allowed
variable patterns (e.g., $home and $env:VAR names) for the value used to set
$target instead of calling ExpandString, or perform a whitelist/regex
substitution of environment variables, falling back to the current
[System.IO.Path]::Combine($dir, $_.item(0)) behavior when no safe expansions are
present; update the code references that call ExpandString (the
$ExecutionContext.InvokeCommand.ExpandString usage computing $target) to use the
new safe-expansion helper.

---

Nitpick comments:
In `@lib/install.ps1`:
- Around line 336-343: The block that expands PATH entries is duplicated;
extract it into a helper function (e.g., Resolve-PathEntry or Expand-PathEntry)
that accepts a directory ($dir) and a single path entry ($_), performs
$ExecutionContext.InvokeCommand.ExpandString($_), compares the expanded value to
the original, and returns either the expanded string or the result of Join-Path
$dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ } to preserve
current behavior; then replace both duplicated blocks (the loop using
$env_add_path.Where and the earlier occurrence) to call this new helper so the
logic (ExpandString, Join-Path, Get-AbsolutePath, is_in_dir) is centralized.
- Around line 322-326: The if-statements in this snippet use `if($path_expand`
(and elsewhere `if($target`) without the project-standard space; update these to
use a space after `if` (e.g., `if ($path_expand -eq $_)`), keeping the same
condition logic and return values for Join-Path/Get-AbsolutePath and the else
branch, and apply the same spacing consistency for any other occurrences like
`if ($target)` or similar in this file; do not change variable names
`$path_expand`, `$dir`, `$_`, or the `is_in_dir` call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3e15c7c-e9d6-4ef6-8858-4d88bd6cae4c

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0334c and 3c05571.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • lib/install.ps1
  • lib/shortcuts.ps1

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/install.ps1`:
- Around line 320-327: The code currently uses
$ExecutionContext.InvokeCommand.ExpandString($_) and compares $path_expand to $_
to decide if a manifest entry contained variables, which misclassifies literal
entries like '$foo\bin' when $foo is undefined; change the logic in the block
that computes $path (and duplicate the same guard in env_rm_path) to detect
variable tokens before trusting ExpandString: if $_ contains a variable token
(e.g. matches '\$[A-Za-z_]' or '\$\{') then use $path_expand, otherwise preserve
the original literal $_ (or if ExpandString produced an empty string while $_
contained '$', treat it as literal). Update the $path_expand/$_ conditional so
literal entries are passed through unchanged and mirror the same fix in
env_rm_path to keep add/remove symmetric.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd6477c3-8a97-4052-9e59-6b428178952f

📥 Commits

Reviewing files that changed from the base of the PR and between 3c05571 and 7611222.

📒 Files selected for processing (2)
  • lib/install.ps1
  • libexec/scoop-reset.ps1

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/install.ps1 (1)

335-341: ⚠️ Potential issue | 🔴 Critical

Same issues apply here; keep symmetric with env_add_path.

This block has the identical security and correctness concerns as env_add_path above. When fixing the expansion logic in env_add_path, apply the same fix here to ensure paths are added and removed consistently.

🔄 Proposed symmetric fix
         $path = $env_add_path.Where({ $_ }) | ForEach-Object {
-            if ($_ -like '$*'){
-                $ExecutionContext.InvokeCommand.ExpandString($_)
-            }else{
+            $usesSupportedVariable = $_ -match '(^|[^`])\$(env:|home\b|\{env:|\{home\})'
+            if ($usesSupportedVariable) {
+                $path_expand = $ExecutionContext.InvokeCommand.ExpandString($_)
+                if ($path_expand -ne $_ -and $path_expand -ne '') {
+                    $path_expand
+                }
+            } else {
                 Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/install.ps1` around lines 335 - 341, The $path construction block
mirroring env_add_path must be changed to use the same secure, symmetric
expansion logic as env_add_path: avoid blind ExpandString on entries that start
with '$*', preserve literal entries and perform Join-Path + Get-AbsolutePath
only for filesystem entries, and ensure is_in_dir($dir, $_) is used
consistently; update the code in the $path = $env_add_path.Where({ $_ }) |
ForEach-Object { ... } block to apply the identical validation, expansion and
path-normalization steps (same checks/functions: ExpandString, Join-Path,
Get-AbsolutePath, is_in_dir) you implemented for env_add_path so additions and
removals behave consistently and safely.
🧹 Nitpick comments (1)
lib/install.ps1 (1)

63-63: Reordering is correct for variable expansion to work.

Moving env_add_path after env_set ensures that any environment variables defined in the manifest's env_set are available in the current session when env_add_path attempts to expand them.

Static analysis suggests using named parameters for clarity:

-    env_add_path $manifest $dir $global $architecture
+    env_add_path -manifest $manifest -dir $dir -global $global -arch $architecture
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/install.ps1` at line 63, The call to env_add_path should use named
parameters for clarity and future-proofing: replace the positional invocation
env_add_path $manifest $dir $global $architecture with a call that passes
parameter names (e.g., -Manifest $manifest -Directory $dir -Global $global
-Architecture $architecture) so readers and static analysis can clearly see
which arguments map to which parameters; ensure you apply this change in the
same invocation and any other places where env_add_path is called.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/install.ps1`:
- Around line 320-326: The expansion branch currently bypasses containment
checks: when $ExecutionContext.InvokeCommand.ExpandString($_) is used on entries
from $env_add_path they are returned without running is_in_dir, allowing
constructs like '$env:WINDIR\System32' or undefined variables to produce paths
outside the app dir; update the expansion branch to (1) restrict which variable
patterns are permitted before calling ExpandString (e.g., only allow a whitelist
like $env:APPDIR/$env:USERPROFILE), (2) after ExpandString perform the same
Join-Path/Get-AbsolutePath/is_in_dir validation used in the non-expanded branch
and reject or log any expanded path that fails containment, and (3) treat
expansions that result in paths starting with a root or that contain unresolved
empty segments (from undefined vars) as invalid and handle them consistently
(log and skip or fail) so $path assignment always contains only validated,
in-dir absolute paths.

---

Duplicate comments:
In `@lib/install.ps1`:
- Around line 335-341: The $path construction block mirroring env_add_path must
be changed to use the same secure, symmetric expansion logic as env_add_path:
avoid blind ExpandString on entries that start with '$*', preserve literal
entries and perform Join-Path + Get-AbsolutePath only for filesystem entries,
and ensure is_in_dir($dir, $_) is used consistently; update the code in the
$path = $env_add_path.Where({ $_ }) | ForEach-Object { ... } block to apply the
identical validation, expansion and path-normalization steps (same
checks/functions: ExpandString, Join-Path, Get-AbsolutePath, is_in_dir) you
implemented for env_add_path so additions and removals behave consistently and
safely.

---

Nitpick comments:
In `@lib/install.ps1`:
- Line 63: The call to env_add_path should use named parameters for clarity and
future-proofing: replace the positional invocation env_add_path $manifest $dir
$global $architecture with a call that passes parameter names (e.g., -Manifest
$manifest -Directory $dir -Global $global -Architecture $architecture) so
readers and static analysis can clearly see which arguments map to which
parameters; ensure you apply this change in the same invocation and any other
places where env_add_path is called.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ce99c3a-da93-4423-8950-f81272e67c75

📥 Commits

Reviewing files that changed from the base of the PR and between 7611222 and 0c22b23.

📒 Files selected for processing (2)
  • lib/install.ps1
  • lib/shortcuts.ps1
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/shortcuts.ps1

Comment on lines +320 to +326
$path = $env_add_path.Where({ $_ }) | ForEach-Object {
if ($_ -like '$*'){
$ExecutionContext.InvokeCommand.ExpandString($_)
}else{
Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
}
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Security: Expanded paths bypass containment validation.

The is_in_dir check that prevents adding arbitrary paths outside the app directory is completely bypassed for expanded variables. A malicious or misconfigured manifest could add system directories to PATH via entries like $env:WINDIR\System32.

Additionally, the past review concern remains unaddressed: undefined variables like $foo\bin will expand to \bin since ExpandString() replaces undefined variables with empty strings, corrupting the intended path.

🛡️ Proposed fix: validate expanded paths and check for supported variables only
         $path = $env_add_path.Where({ $_ }) | ForEach-Object {
-            if ($_ -like '$*'){
-                $ExecutionContext.InvokeCommand.ExpandString($_)
-            }else{
+            # Only expand known safe variables ($env:*, $home)
+            $usesSupportedVariable = $_ -match '(^|[^`])\$(env:|home\b|\{env:|\{home\})'
+            if ($usesSupportedVariable) {
+                $path_expand = $ExecutionContext.InvokeCommand.ExpandString($_)
+                # Only use expanded value if expansion actually changed it
+                # (undefined variables expand to empty, corrupting the path)
+                if ($path_expand -ne $_ -and $path_expand -ne '') {
+                    $path_expand
+                }
+            } else {
                 Join-Path $dir $_ | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
             }
         }

Consider whether expanded paths should also be validated against a whitelist of allowed directories, or at minimum logged for audit purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/install.ps1` around lines 320 - 326, The expansion branch currently
bypasses containment checks: when
$ExecutionContext.InvokeCommand.ExpandString($_) is used on entries from
$env_add_path they are returned without running is_in_dir, allowing constructs
like '$env:WINDIR\System32' or undefined variables to produce paths outside the
app dir; update the expansion branch to (1) restrict which variable patterns are
permitted before calling ExpandString (e.g., only allow a whitelist like
$env:APPDIR/$env:USERPROFILE), (2) after ExpandString perform the same
Join-Path/Get-AbsolutePath/is_in_dir validation used in the non-expanded branch
and reject or log any expanded path that fails containment, and (3) treat
expansions that result in paths starting with a root or that contain unresolved
empty segments (from undefined vars) as invalid and handle them consistently
(log and skip or fail) so $path assignment always contains only validated,
in-dir absolute paths.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More save solution would be to use Scoop core substitute function.

$envSubstitutes = @{}
Get-ChildItem Env:* | ForEach-Object { $envSubstitutes['$' + $_.Name] = $_.Value }
$path = $env_add_path.Where({ $_ }).ForEach({ Join-Path $dir $_ | Get-AbsolutePath | ForEach-Object { substitute $_ $envSubstitutes } }).Where({ is_in_dir $dir $_ })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Let me look at the substitute function implementation to understand its behavior.


🏁 Script executed:

#!/bin/bash
ast-grep --pattern 'function substitute($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
rg -n "function substitute" -A 15

Length of output: 1074


@chendler, the substitute approach is appealing because it preserves the is_in_dir containment check. However, there are a few issues with the proposed implementation worth considering:

  1. Substitution happens after Join-Path — for an entry like $env:ProgramFiles\Git\bin, the pipeline first produces <appdir>\$env:ProgramFiles\Git\bin, and then substitution transforms it into <appdir>\C:\Program Files\Git\bin, which is a garbled path.

  2. Key format mismatch — the mapping uses '$' + $_.Name (e.g., $PROGRAMFILES), but the issue examples (#6605) use $env:ProgramFiles style. These keys won't match.

  3. PowerShell automatic variables (e.g., $home) aren't in Env:*Get-ChildItem Env:* only yields Windows environment variables, so $home\.local\bin would never be substituted.

  4. is_in_dir will always reject absolute expanded paths — even if substitution worked, an expanded absolute path like C:\Program Files\Git\bin is intentionally outside $dir, so it gets silently dropped by the .Where({ is_in_dir $dir $_ }) filter, meaning no variable-based absolute path would ever actually be added.

A corrected version that addresses these points would need to substitute before Join-Path, include both $env:NAME and $NAME keys, add PowerShell automatic variables, and then branch on whether the result is an absolute or relative path:

$envSubstitutes = @{}
Get-ChildItem Env:* | ForEach-Object {
    $envSubstitutes['$env:' + $_.Name] = $_.Value  # $env:ProgramFiles style
    $envSubstitutes['$' + $_.Name] = $_.Value      # bare $ProgramFiles style
}
$envSubstitutes['$home'] = $HOME  # PowerShell automatic variable

$path = $env_add_path.Where({ $_ }) | ForEach-Object {
    $expanded = substitute $_ $envSubstitutes
    if ([System.IO.Path]::IsPathRooted($expanded)) {
        # Absolute path from variable expansion — skip is_in_dir but validate it exists/is valid
        $expanded | Where-Object { Test-Path $_ -PathType Container -IsValid }
    } else {
        Join-Path $dir $expanded | Get-AbsolutePath | Where-Object { is_in_dir $dir $_ }
    }
}

That said, skipping is_in_dir for expanded absolute paths is an intentional trade-off — it's the whole point of the feature. The security question then shifts to whether manifests in trusted buckets are sufficient gatekeeping, or whether an explicit allowlist of variable prefixes (e.g., only $env:* and $home) is warranted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I can agree that $env: can be used as a prefix

  2. $home is just an alias to $env:USERPROFILE

  3. Omitting is_in_dir check will contradict manifest spec defined here https://github.com/ScoopInstaller/Scoop/wiki/App-Manifests#optional-properties

env_add_path: Add this directory to the user's path (or system path if --global is used). The directory is relative to the install directory and must be inside the install directory.

I assume that features built-in features prevent side effects, for other special cases there is post_install script.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chendler, all three points are well-taken. Agreed on all counts:

  1. Only $env: prefix needed.
  2. $home is redundant — $env:USERPROFILE covers it.
  3. The is_in_dir check must be preserved per the manifest spec. That constraint also provides a natural security boundary: any variable that expands to an absolute path outside $dir will fail the containment check and be silently dropped, which is the correct behavior.

With those constraints in mind, here's the corrected implementation that substitutes $env: variables before Join-Path (fixing the ordering issue from my earlier comment) while keeping the existing containment pipeline intact:

function env_add_path($manifest, $dir, $global, $arch) {
    $env_add_path = arch_specific 'env_add_path' $manifest $arch
    $dir = $dir.TrimEnd('\')
    if ($env_add_path) {
        if (get_config USE_ISOLATED_PATH) {
            Add-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global
        }
        $envSubstitutes = @{}
        Get-ChildItem Env:* | ForEach-Object { $envSubstitutes['$env:' + $_.Name] = $_.Value }
        $path = $env_add_path.Where({ $_ }).ForEach({
            Join-Path $dir (substitute $_ $envSubstitutes) | Get-AbsolutePath
        }).Where({ is_in_dir $dir $_ })
        Add-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global -Force
    }
}

And symmetrically for env_rm_path:

function env_rm_path($manifest, $dir, $global, $arch) {
    $env_add_path = arch_specific 'env_add_path' $manifest $arch
    $dir = $dir.TrimEnd('\')
    if ($env_add_path) {
        $envSubstitutes = @{}
        Get-ChildItem Env:* | ForEach-Object { $envSubstitutes['$env:' + $_.Name] = $_.Value }
        $path = $env_add_path.Where({ $_ }).ForEach({
            Join-Path $dir (substitute $_ $envSubstitutes) | Get-AbsolutePath
        }).Where({ is_in_dir $dir $_ })
        Remove-Path -Path $path -Global:$global
        Remove-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global
    }
}

Key properties of this approach:

  • substitute uses -replace (case-insensitive in PowerShell), so $env:ProgramFiles, $env:PROGRAMFILES, and $env:programfiles all match correctly against the hashtable built from Get-ChildItem Env:*.
  • No special-casing needed — the $_ -like '$*' branch is gone entirely; every entry goes through the same pipeline.
  • is_in_dir is always enforced — if an env var happens to expand to an absolute path, Join-Path $dir <absolute> produces a garbled path that fails containment, so it gets dropped. Intentional absolute paths outside the app dir belong in post_install, as you noted.

✏️ Learnings added
Learnt from: chendler
Repo: ScoopInstaller/Scoop PR: 6616
File: lib/install.ps1:320-326
Timestamp: 2026-03-21T09:08:18.606Z
Learning: In Scoop's `env_add_path` and `env_rm_path` functions in `lib/install.ps1` (PowerShell), the manifest spec requires env_add_path entries to be relative to and inside the install directory (`is_in_dir` check must always be enforced). Variable expansion for these fields should only support `$env:NAME` style prefixes (not bare `$home`, as it equals `$env:USERPROFILE`), should be done using the existing `substitute` function with a hashtable built from `Get-ChildItem Env:*` keyed as `'$env:' + $_.Name`, and substitution must occur BEFORE `Join-Path $dir` so the containment pipeline remains intact. Bypassing `is_in_dir` via `ExpandString` contradicts the manifest spec.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@abgox
Copy link
Copy Markdown
Author

abgox commented Mar 21, 2026

env_add_path: Add this directory to the user's path (or system path if --global is used). The directory is relative to the install directory and must be inside the install directory.

  • This PR, as an extension, will inherently break this rule, because paths containing environment variables are generally not in Scoop's installation directory.

  • What we need to consider is whether this rule makes sense for applications that rely on external paths.

  • If it does make sense, why do some manifests still need to manually call the internal method Add-Path, such as g in Main.

  • For such a situation, would it be more controllable to have the field handle it itself than to manually call the internal method Add-Path?

  • I think these points should be discussed in [Feature] Allow env_add_path and shortcuts field to use variables #6605 to determine whether this PR needs to be continued.

@abgox
Copy link
Copy Markdown
Author

abgox commented Mar 21, 2026

  • Refer to the implementation logic in env_set, which directly uses $ExecutionContext.InvokeCommand.ExpandString().

    Scoop/lib/install.ps1

    Lines 335 to 346 in 0d0334c

    function env_set($manifest, $global, $arch) {
    $env_set = arch_specific 'env_set' $manifest $arch
    if ($env_set) {
    $env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
    $name = $_.Name
    $val = $ExecutionContext.InvokeCommand.ExpandString($env_set.$($name))
    Write-Output "Setting $(if ($global) {'system'} else {'user'}) environment variable: $([char]0x1b)[34m$name$([char]0x1b)[0m = $([char]0x1b)[35m$val$([char]0x1b)[0m"
    Set-EnvVar -Name $name -Value $val -Global:$global
    Set-Content env:\$name $val
    }
    }
    }

  • Following this logic, we should probably use $ExecutionContext.InvokeCommand.ExpandString() directly and then compare whether the old and new values are the same.

  • If they are the same, it means no variables are included, and the original logic will be executed.

  • If they are different, it means environment variables exist, and the expanded values will be used instead.

  • Refer to https://github.com/abgox/Scoop/blob/761122247a3e5e5688c45693e6e5782c9667db45/lib/install.ps1#L320-L327

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.

2 participants