feat: add Linux system-wide setup#376
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #376 +/- ##
==========================================
- Coverage 55.78% 55.72% -0.07%
==========================================
Files 217 219 +2
Lines 14781 15249 +468
==========================================
+ Hits 8246 8497 +251
- Misses 5790 5978 +188
- Partials 745 774 +29 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Tighten shim detection, profile repair, and install ordering while trimming over-specific doctor/info hints from the system-install path. Co-authored-by: Cursor <cursoragent@cursor.com>
3635f35 to
ffd0e7e
Compare
Use UserBinDir for PATH checks and pass aliases as not required under system install without treating that as active interception. Co-authored-by: Cursor <cursoragent@cursor.com>
Prefix the warning with [pmg] and drop the redundant continuing clause. Co-authored-by: Cursor <cursoragent@cursor.com>
SafeDep Report SummaryPackage Details
This report is generated by SafeDep Github App |
Cover root system setup, PATH/profile.d, managed config, non-root interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e job after an integrity crash on pnpm add. Co-authored-by: Cursor <cursoragent@cursor.com>
Align package.json with the pnpm version we want in CI so action-setup stops erroring on a version mismatch after the e2e integrity flake. Co-authored-by: Cursor <cursoragent@cursor.com>
pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then fails after PMG analysis even on 11.10.0. Seed the temp package with npm init instead. Co-authored-by: Cursor <cursoragent@cursor.com>
The e2e integrity crash is avoided by npm init; the 11.10.0 bump is no longer needed. Co-authored-by: Cursor <cursoragent@cursor.com>
abhisek
left a comment
There was a problem hiding this comment.
Deep-dive review of the system-install feature. Overall: the design is sound and rides well on existing infrastructure (managed/global config, PMG_SHIM_PATH stripping), the docs are genuinely good, and the new e2e-system-install CI job is a strong addition — it covers the flows unit tests can't reach, including non-root interception through the system shims and clean removal. Idempotent install with profile self-repair, usefulerror codes for the root/platform gates, and the install-order comment in installSystem are all appreciated.
Issues, in priority order (details inline):
- Event-logging fail-open is a global policy change — explicit
--logfailures should stay fatal, and lockdown (IsLocked()) should keep enforcement; soft-fail is right as the default. (main.go) validateSystemExecutableonly checkso+x— a user-writable binary path baked into root-installed shims is a lateral-movement vector on multi-user machines. (internal/shim/system.go)remove --systemis blocked by the same validation — uninstall should not require a valid executable. (cmd/setup/setup.go)- PATH precedence isn't verified — nvm/pyenv-style rc prepends silently bypass system shims while doctor reports green on membership. (cmd/setup/doctor.go, docs)
isInterceptionActivebranches on a display string — message text is now load-bearing logic, also grepped by CI. (cmd/setup/doctor.go)- Smaller items: install output should mention that per-user configs become ignored;
writeSystemProfilereads the global bin dir instead of the manager's config.
Nits not worth inline threads: errIfSystemInstallAllowed reads as "error if allowed" — consider requireSystemInstallSupported; per-user Remove() now aborts before rc cleanup when shim-dir removal fails (consider attempting rc cleanup and joining errors); the pnpm-init CI flake fix is unrelated to this feature and could land separately.
I'd hold merge on 1 and 2; the rest is follow-up-able.
Generated by Claude Code
Require root-owned, non-group/other-writable pmg for --system install; allow remove without that validation. Doctor checks npm resolution for PATH precedence, uses ImpliesInterception instead of message matching, and documents version-manager shadowing. Pass profile bin dir from the shim manager and note that system config ignores per-user files. Co-authored-by: Cursor <cursoragent@cursor.com>
Doctor now verifies every installed package manager against the shim directory, and system-install validation only requires a safe parent directory. Cloud sync records username/uid on invocation context for multi-user hosts sharing one endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
vet Summary ReportThis report is generated by vet Policy Checks
Malicious Package AnalysisMalicious package analysis was performed using SafeDep Cloud API Malicious Package Analysis Report
Changed PackagesChanged Packages
|
- shim: make system executable resolution injectable so tests pass under umask 002; skip the root-owner test when running as root - doctor: treat resolution into either the system or per-user shim dir as intercepted, and collapse the shim-in-PATH check to a single call site - setup: make remove (both --system and per-user) best-effort with errors.Join so one failed step no longer strands the other artifact - shim: allow a group-writable install parent dir (Debian/Ubuntu ship /usr/local/bin as root:staff 2775) while still rejecting world-writable and non-root-owned parents - audit: attribute cloud events to SUDO_USER when running under sudo - docs: drop the soft-fail event-logging claim (hard-fail is retained)
The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so tooling can install without sudo. System install correctly refuses a world-writable dir for the shared binary (any local user could replace it and hijack every user's npm/pip). No FHS-compliant distro or Docker image ships it world-writable — it is always root:root 0755 or root:staff 2775 — so this normalizes only the anomalous CI runner back to standard perms and still exercises the real /usr/local/bin path.
A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su without -) creates the invoking user's ~/.config/safedep as root-owned, and event-log init then fail-closes every later non-root command. Make that state self-solvable: - event-log init permission errors exit with a usefulerror naming the likely cause and the chown fix instead of a bare fatal - pmg setup doctor probes event-log dir writability and reports the same fix via a new per-result Fix override - document the mechanism and remedy in system-install.md, along with the binary ownership requirements for --system - consolidate this branch's doctor tests into doctor_test.go
Path resolution trusted HOME (and XDG_*), which sudo and su can preserve from the invoking user (GitHub runners, sudo -E, su without -). Any pmg run as root then created root-owned ~/.config/safedep inside that user's home, and event-log init fail-closed every later non-root pmg/npm/pip run for them. System install made sudo pmg the documented flow, turning this latent bug into the happy path. When euid is 0, configDir and cacheDir now resolve from root's passwd home instead of the environment, so root state lands under /root and user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win, non-root resolution is unchanged, and Windows is unaffected (no euid). Event-log init stays fatal on failure; sudo-run package events are attributed via SUDO_USER and synced by the exit auto-sync as usual. E2E: GitHub runners preserve HOME under sudo, so assert that no sudo pmg run leaks state into the runner's home, and that the managed-config refusal fails for the documented reason rather than a permission brick.
The chown hint is only correct when another account created files inside the current user's own home. When a leaked HOME or XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub runners), following it would chown that user's directory and brick their pmg instead. Classify the failure against the passwd home, which the leaked environment cannot influence, and prescribe: - dir inside own home: restore ownership with chown - dir outside own home: fix the leaked environment, never chown - explicit PMG_CONFIG_DIR: make it writable Used by both the fatal event-log error and the doctor check, and the docs troubleshooting now carries the same two-case triage.
GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks through sudo -u, so the pmgtest pmg resolved the runner user's config dir and fail-closed on its runner-owned log file (run 29289868727 shows the triaged error catching exactly this). Set it inside the login shell so it wins regardless of how the leak is delivered. The remedy now returns a full-help and doctor-table pair from a single triage, and drops the do-not-chown tail from the leak message.
Belongs with the previous commit; it was left unstaged and e0580b1 does not compile without it.
Running as uid 0 without a resolvable root passwd entry (scratch containers, minimal chroots) panicked at startup on every command, because the euid-based path resolution had no fallback. Fall back to env-derived resolution there: without a passwd database there is no user switching, so the cross-user poisoning that branch prevents cannot occur. Also restore the underlying cause in the generic event-log init error (minimal output hid it after the usefulerror change), and document that root's per-user data lives under /root regardless of a preserved HOME.
|
Follow-up on the latest push (
Verified with an A/B run in the same root container:
CI stays green only because GitHub-hosted runners run as non-root Two ways to fix, depending on the intended contract:
Option 1 seems right — it matches the PR's threat model and its own e2e step (which fixes leaked env for the non-root user, where env is still honored). Might also be worth adding a root Generated by Claude Code |
There was a problem hiding this comment.
Pull request overview
Adds a Linux “system-wide install” path so a single PMG installation can protect all users on shared VMs and baked images, with stronger install-time validation and better diagnostics for common sudo/root environment pitfalls.
Changes:
- Introduces
pmg setup install --system/pmg setup remove --system(Linux, root) with system shim/profile management and managed config at OS paths. - Hardens root-run behavior around per-user state location (config/cache) and improves fatal event-log init errors plus
pmg setup doctorchecks/remediation. - Adds CI/E2E coverage for system install flows and updates docs to cover usage, Docker implications, and troubleshooting.
Reviewed changes
Copilot reviewed 30 out of 31 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds a pointer to Linux system-wide setup docs. |
| main.go | Converts event-log init failures into categorized usefulerror with a permission-specific remedy. |
| internal/ui/info.go | Adds post-install UX output for system-wide setup (paths + Docker PATH hint). |
| internal/shim/system.go | Implements system shim manager, executable safety validation, and /etc/profile.d snippet management. |
| internal/shim/system_test.go | Adds unit tests for system shim install/remove, profile writing, and executable validation. |
| internal/shim/shim.go | Extends shim config for system mode (skip rc edits/manage profile), best-effort removal, and shim detection helpers. |
| internal/shim/shim_test.go | Adds coverage for error-returning behavior in Remove(). |
| internal/shim/path.go | Ensures PATH filtering strips the system shim directory too. |
| internal/shim/path_test.go | Adds a test for stripping the system shim dir without env vars. |
| internal/shim/file_owner_windows.go | Adds Windows stub for owner UID resolution. |
| internal/shim/file_owner_unix.go | Adds Unix owner UID resolution for executable ownership checks. |
| internal/doctor/doctor.go | Extends doctor results with ImpliesInterception and per-result fix overrides. |
| internal/audit/cloud_sink.go | Attributes cloud events to the invoking user (prefers SUDO_USER). |
| internal/audit/cloud_sink_test.go | Asserts username/uid are present in emitted invocation context. |
| go.work.sum | Updates dependency checksums. |
| go.sum | Updates dependency checksums for the bumped API protos. |
| go.mod | Bumps buf.build/gen/go/.../protocolbuffers module version. |
| docs/system-install.md | New documentation for Linux system install, Docker PATH, limitations, and troubleshooting. |
| docs/persistent-proxy.md | Updates system-install reference to point at the new doc. |
| docs/development.md | Adds system-install doc to the development docs index. |
| docs/config.md | Links to system install doc from config docs. |
| config/rootdir_test.go | Tests root path resolution ignoring poisoned HOME/XDG env vars. |
| config/remedy_test.go | Tests triaged help text for unwritable config/event-log directories. |
| config/managed_config_test.go | Adds tests for writing/removing the system-managed template config. |
| config/config.go | Implements root config/cache dir resolution from passwd home, triaged remedies, and system-managed config helpers. |
| cmd/setup/setup.go | Adds --system install/remove modes, root requirement enforcement, and best-effort removal behavior. |
| cmd/setup/setup_test.go | Adds tests for Linux/root gating on --system. |
| cmd/setup/info.go | Extends setup info output to include system shim state. |
| cmd/setup/doctor.go | Updates doctor to understand system installs, PATH resolution, and triaged event-log writability fixes. |
| cmd/setup/doctor_test.go | Adds targeted unit tests for new doctor resolution logic and interception inference. |
| .github/workflows/pmg-e2e.yml | Adds a dedicated E2E job for system install (binary rejection, artifacts, PATH activation, managed config, non-root interception, uninstall). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…check Address review findings on the system-install PR: - cloud_sink: honor SUDO_USER for audit attribution only when euid==0. Without the gate any user could set SUDO_USER and spoof cloud-audit attribution to another account. Matches the guard in cmd/setup/cert.go. - config: divert per-user paths to root's passwd home only on an actual sudo elevation (euid==0 && SUDO_USER set), not for every root euid. The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently stopped reading genuine root users' config (golden Docker images), regressing two tests that only fail when the suite runs as root. Genuine root honors the environment as before; su without - leaves no marker and stays a documented, loud-failing residual. - doctor: add a system-only check re-validating that the binary the installed shims exec is still root-owned and non-writable, catching permission/ownership drift after install. - shim: fold the duplicated shim-scan loop into firstShimContent.
Address remaining review comments: - shim: force root:root 0755 on the managed system dirs (shim tree and profile.d) after MkdirAll. A pre-created dir with weaker ownership, possible under Debian's group-writable /usr/local/lib, would let a non-root user replace the shims every account executes. - audit: when SUDO_USER has no passwd entry (minimal containers), attribute cloud events from sudo's recorded SUDO_USER/SUDO_UID env instead of falling back to root. Still gated on euid 0. - setup: reword the root-without---system warning; alias/shim install follows HOME, so claiming it configures only root's home was wrong. - shim: skip the non-root-owner validation test on Windows, where file ownership is not resolvable.
The system-install validation checked the binary's own permissions and the parent's tamper-safety but never reachability: a 0755 root-owned binary under a 0700 directory (e.g. /root/pmg) passed every check while every non-root user's shim failed with exit 127. Walk the directory chain to / and require the search bit for others; doctor's system binary check inherits this. E2E gains a reject case for a binary under a non-searchable directory. setup info: render alias/user-shim/system-shim rows through one installed-state formatter (location when installed, "not installed" otherwise) instead of a mix of booleans, paths, and prose.
writeSystemProfile chowned/chmod'd /etc/profile.d itself, a shared system directory pmg does not own, silently overriding any perms a sysadmin set on it. Secure only the file pmg writes (pmg.sh) via secureSystemFile, which also forces 0644 explicitly so a restrictive root umask cannot leave the snippet non-world-readable (which would drop the shim dir from other users' login-shell PATH). Docs: add Limitations entries for the group-writable install dir bypass (validation is defeatable on non-sticky group-writable dirs like Debian's /usr/local/bin) and the elevation-only scope (su without - can still poison the caller's home; sudo -u cannot poison another account). Trim the requireSafeParentDir comment to a pointer.
| // realUserHomeDir returns the current user's home from the passwd database, | ||
| // ignoring HOME and XDG_* env vars that may be leaked from another account. | ||
| // Overridable in tests. | ||
| var realUserHomeDir = func() (string, error) { |
There was a problem hiding this comment.
This is ambiguous. What is "real"? It should be "currentUserHomeDir"
| if err != nil { | ||
| return "", err | ||
| } | ||
| return u.HomeDir, nil |
There was a problem hiding this comment.
Check and fail if u.HomeDir is empty?
| // belongs to another account steals it and bricks that account instead, so | ||
| // its is only suggested when the directory is inside the current user's | ||
| // real (passwd) home, which a leaked environment cannot influence. | ||
| func UnwritableConfigDirRemedy(dir string) (help, fix string) { |
There was a problem hiding this comment.
What is the purpose of this function? Only to return some remedy? If "dir" is the target, then why is it touching pmgConfigDirEnvKey, current user home dir?
From the description, the function looks overloaded and violates SRP:
returns actionable help for a per-user config or event-log directory
Address the open review threads on #376: - rename realUserHomeDir to currentUserHomeDir and fail when the passwd entry has no home directory - split UnwritableConfigDirRemedy into classifyUnwritableDir (cause diagnosis) and pure message rendering so each function has one job - replace cmd/setup's duplicate pathIsUnderDir with the shared config.PathWithinDir, now guarding empty inputs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A
* feat: add Linux system-wide setup Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: keep local design documents untracked Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden and simplify Linux system install Tighten shim detection, profile repair, and install ordering while trimming over-specific doctor/info hints from the system-install path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: clarify system-install doctor alias and shim path checks Use UserBinDir for PATH checks and pass aliases as not required under system install without treating that as active interception. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: tighten event-log soft-fail warning prefix Prefix the warning with [pmg] and drop the redundant continuing clause. Co-authored-by: Cursor <cursoragent@cursor.com> * ci: add Linux system-install e2e and pin pnpm for add flake Cover root system setup, PATH/profile.d, managed config, non-root interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e job after an integrity crash on pnpm add. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: bump packageManager to pnpm 11.10.0 for e2e Align package.json with the pnpm version we want in CI so action-setup stops erroring on a version mismatch after the e2e integrity flake. Co-authored-by: Cursor <cursoragent@cursor.com> * ci: use npm init for pnpm e2e to avoid integrity crash pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then fails after PMG analysis even on 11.10.0. Seed the temp package with npm init instead. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: revert packageManager pin to pnpm 11.1.3 The e2e integrity crash is avoided by npm init; the 11.10.0 bump is no longer needed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden system-install review findings Require root-owned, non-group/other-writable pmg for --system install; allow remove without that validation. Doctor checks npm resolution for PATH precedence, uses ImpliesInterception instead of message matching, and documents version-manager shadowing. Pass profile bin dir from the shim manager and note that system config ignores per-user files. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden doctor PATH checks and attribute cloud events by OS user Doctor now verifies every installed package manager against the shim directory, and system-install validation only requires a safe parent directory. Cloud sync records username/uid on invocation context for multi-user hosts sharing one endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: address system-install review findings - shim: make system executable resolution injectable so tests pass under umask 002; skip the root-owner test when running as root - doctor: treat resolution into either the system or per-user shim dir as intercepted, and collapse the shim-in-PATH check to a single call site - setup: make remove (both --system and per-user) best-effort with errors.Join so one failed step no longer strands the other artifact - shim: allow a group-writable install parent dir (Debian/Ubuntu ship /usr/local/bin as root:staff 2775) while still rejecting world-writable and non-root-owned parents - audit: attribute cloud events to SUDO_USER when running under sudo - docs: drop the soft-fail event-logging claim (hard-fail is retained) * ci: normalize /usr/local/bin perms before system-install e2e The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so tooling can install without sudo. System install correctly refuses a world-writable dir for the shared binary (any local user could replace it and hijack every user's npm/pip). No FHS-compliant distro or Docker image ships it world-writable — it is always root:root 0755 or root:staff 2775 — so this normalizes only the anomalous CI runner back to standard perms and still exercises the real /usr/local/bin path. * fix: actionable remedy for root-created per-user config dir A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su without -) creates the invoking user's ~/.config/safedep as root-owned, and event-log init then fail-closes every later non-root command. Make that state self-solvable: - event-log init permission errors exit with a usefulerror naming the likely cause and the chown fix instead of a bare fatal - pmg setup doctor probes event-log dir writability and reports the same fix via a new per-result Fix override - document the mechanism and remedy in system-install.md, along with the binary ownership requirements for --system - consolidate this branch's doctor tests into doctor_test.go * fix: resolve per-user paths from root's own home when running as root Path resolution trusted HOME (and XDG_*), which sudo and su can preserve from the invoking user (GitHub runners, sudo -E, su without -). Any pmg run as root then created root-owned ~/.config/safedep inside that user's home, and event-log init fail-closed every later non-root pmg/npm/pip run for them. System install made sudo pmg the documented flow, turning this latent bug into the happy path. When euid is 0, configDir and cacheDir now resolve from root's passwd home instead of the environment, so root state lands under /root and user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win, non-root resolution is unchanged, and Windows is unaffected (no euid). Event-log init stays fatal on failure; sudo-run package events are attributed via SUDO_USER and synced by the exit auto-sync as usual. E2E: GitHub runners preserve HOME under sudo, so assert that no sudo pmg run leaks state into the runner's home, and that the managed-config refusal fails for the documented reason rather than a permission brick. * fix: triage the unwritable config dir remedy by cause The chown hint is only correct when another account created files inside the current user's own home. When a leaked HOME or XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub runners), following it would chown that user's directory and brick their pmg instead. Classify the failure against the passwd home, which the leaked environment cannot influence, and prescribe: - dir inside own home: restore ownership with chown - dir outside own home: fix the leaked environment, never chown - explicit PMG_CONFIG_DIR: make it writable Used by both the fatal event-log error and the doctor check, and the docs troubleshooting now carries the same two-case triage. * ci: pin XDG_CONFIG_HOME for the cross-user e2e step; terse doctor fix GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks through sudo -u, so the pmgtest pmg resolved the runner user's config dir and fail-closed on its runner-owned log file (run 29289868727 shows the triaged error catching exactly this). Set it inside the login shell so it wins regardless of how the leak is delivered. The remedy now returns a full-help and doctor-table pair from a single triage, and drops the do-not-chown tail from the leak message. * fix: adapt event-log error to the two-value remedy signature Belongs with the previous commit; it was left unstaged and e0580b1 does not compile without it. * fix: fall back to env path resolution when root has no passwd entry Running as uid 0 without a resolvable root passwd entry (scratch containers, minimal chroots) panicked at startup on every command, because the euid-based path resolution had no fallback. Fall back to env-derived resolution there: without a passwd database there is no user switching, so the cross-user poisoning that branch prevents cannot occur. Also restore the underlying cause in the generic event-log init error (minimal output hid it after the usefulerror change), and document that root's per-user data lives under /root regardless of a preserved HOME. * fix: gate SUDO_USER trust and root path diversion; add doctor binary check Address review findings on the system-install PR: - cloud_sink: honor SUDO_USER for audit attribution only when euid==0. Without the gate any user could set SUDO_USER and spoof cloud-audit attribution to another account. Matches the guard in cmd/setup/cert.go. - config: divert per-user paths to root's passwd home only on an actual sudo elevation (euid==0 && SUDO_USER set), not for every root euid. The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently stopped reading genuine root users' config (golden Docker images), regressing two tests that only fail when the suite runs as root. Genuine root honors the environment as before; su without - leaves no marker and stays a documented, loud-failing residual. - doctor: add a system-only check re-validating that the binary the installed shims exec is still root-owned and non-writable, catching permission/ownership drift after install. - shim: fold the duplicated shim-scan loop into firstShimContent. * fix: harden system dirs at install; keep sudo attribution without passwd Address remaining review comments: - shim: force root:root 0755 on the managed system dirs (shim tree and profile.d) after MkdirAll. A pre-created dir with weaker ownership, possible under Debian's group-writable /usr/local/lib, would let a non-root user replace the shims every account executes. - audit: when SUDO_USER has no passwd entry (minimal containers), attribute cloud events from sudo's recorded SUDO_USER/SUDO_UID env instead of falling back to root. Still gated on euid 0. - setup: reword the root-without---system warning; alias/shim install follows HOME, so claiming it configures only root's home was wrong. - shim: skip the non-root-owner validation test on Windows, where file ownership is not resolvable. * fix: reject system binaries unreachable by other users; consistent info The system-install validation checked the binary's own permissions and the parent's tamper-safety but never reachability: a 0755 root-owned binary under a 0700 directory (e.g. /root/pmg) passed every check while every non-root user's shim failed with exit 127. Walk the directory chain to / and require the search bit for others; doctor's system binary check inherits this. E2E gains a reject case for a binary under a non-searchable directory. setup info: render alias/user-shim/system-shim rows through one installed-state formatter (location when installed, "not installed" otherwise) instead of a mix of booleans, paths, and prose. * fix: stop reowning /etc/profile.d; document group-writable and su gaps writeSystemProfile chowned/chmod'd /etc/profile.d itself, a shared system directory pmg does not own, silently overriding any perms a sysadmin set on it. Secure only the file pmg writes (pmg.sh) via secureSystemFile, which also forces 0644 explicitly so a restrictive root umask cannot leave the snippet non-world-readable (which would drop the shim dir from other users' login-shell PATH). Docs: add Limitations entries for the group-writable install dir bypass (validation is defeatable on non-sticky group-writable dirs like Debian's /usr/local/bin) and the elevation-only scope (su without - can still poison the caller's home; sudo -u cannot poison another account). Trim the requireSafeParentDir comment to a pointer. * refactor: separate unwritable-dir diagnosis from remedy rendering Address the open review threads on #376: - rename realUserHomeDir to currentUserHomeDir and fail when the passwd entry has no home directory - split UnwritableConfigDirRemedy into classifyUnwritableDir (cause diagnosis) and pure message rendering so each function has one job - replace cmd/setup's duplicate pathIsUnderDir with the shared config.PathWithinDir, now guarding empty inputs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A * fix: repair umask-clamped modes on system shims and managed config os.WriteFile and os.MkdirAll honor the process umask, so a hardened root umask (e.g. 077) produced 0700 shims other users cannot execute and a 0600/0700 managed config non-root pmg runs cannot read - silently disabling the system-wide policy. Chmod/chown the artifacts explicitly after writing, with unix regression tests running under umask 077. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A * refactor: address system-install maintainability review - move PathWithinDir and the root-owned mkdir/chmod helpers into a new internal/fsutil package; config no longer exports a generic fs util - create managed-config directories with MkdirAllRootOwned, which only sets ownership and mode on directories it creates - drops the hardcoded safedep parent-dir heuristic and never touches pre-existing directories - collapse NewSystemShimManagerForRemove into NewSystemShimManager and validate the executable in Install, where the action needs it; Remove works regardless of binary state - rename ShimConfig.ManageProfile to SystemProfile and document it - name the Linux-only system paths linuxSystemBinDir/ProfilePath and document the Unix-only validation semantics - share the PMG_BIN shim variable name between writeShimScript and parseShimPMGBin via the shimPMGBinVar constant - cloud attribution falls back to the effective uid when SUDO_UID is absent, so sudo-invoked commands are not misattributed to root Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A --------- Co-authored-by: Sahilb315 <bansalsahil315@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude <noreply@anthropic.com>




Summary
Adds Linux system-wide setup so one install protects every user account on shared VMs and golden Docker images.
pmg setup install --system(Linux, root): package-manager shims in/usr/local/lib/pmg/bin, managed config at/etc/safedep/pmg/config.yml, login-shell PATH via/etc/profile.d/pmg.sh.pmg setup remove --systemuninstalls; removal is best-effort so one failed step does not strand the rest./usr/local/binasroot:staff2775.$HOME. Environments where sudo/su preserve the invoking user's HOME previously made anysudo pmgrun create root-owned state inside that user's home, fail-closing every later non-root pmg/npm/pip run for them.HOME/XDG_CONFIG_HOMEpoints at another user's home (chown there would brick the other account), make the directory writable whenPMG_CONFIG_DIRis explicit.pmg setup doctorunderstands system installs: alias and shim checks, a resolution-based PATH check that accepts either shim directory, and an event-log writability probe with the same triaged fix.SUDO_USER.docs/system-install.mdcovers setup, DockerENV PATH, limitations, user data directories, and troubleshooting.Made with Cursor