33// from a layered set of sources so callers can stop deriving
44// ~/.stepsecurity independently:
55//
6- // 1. --install-dir CLI flag (set by main via SetOverride)
7- // 2. $STEPSECURITY_HOME environment variable (set by service unit / loader )
8- // 3. install_dir config field (loaded by internal/config )
6+ // 1. --install-dir CLI flag (set by main via SetOverride / SetDisabled )
7+ // 2. install_dir config field (loaded by internal/config )
8+ // 3. $STEPSECURITY_HOME environment variable (fallback only )
99// 4. ~/.stepsecurity (legacy default)
1010//
11+ // Why config beats env: the install_dir field in config.json is the
12+ // canonical source of truth — the loader scripts (agent-api) write to
13+ // it on every run, and operators can hand-edit it. Service installers
14+ // (launchd / systemd / schtasks) bake STEPSECURITY_HOME into their unit
15+ // files at install time, but that snapshot becomes stale the moment the
16+ // operator edits config.json. Letting config win means scheduler-
17+ // invoked runs immediately reflect a config change without requiring
18+ // the operator to re-run `install`. The env var stays as a defensive
19+ // fallback for the rare case where config.json is unreadable.
20+ //
1121// config.json itself stays at the legacy location regardless — see
1222// internal/config.LegacyDir — so the agent can always bootstrap. All
1323// other files (logs, hook errors, the binary placed by the loader) live
@@ -22,46 +32,73 @@ import (
2232 "github.com/step-security/dev-machine-guard/internal/config"
2333)
2434
25- // HomeEnvVar is the environment variable consulted in resolution
26- // step 2. Service installers bake this into their unit files so
27- // scheduler-invoked runs see the same install dir as interactive ones.
35+ // HomeEnvVar is the environment variable consulted as a defensive
36+ // fallback when both the CLI override and config.InstallDir are unset.
37+ // Service installers bake this into their unit files but its precedence
38+ // is intentionally below config so that an edited install_dir wins.
2839const HomeEnvVar = "STEPSECURITY_HOME"
2940
3041// cliOverride captures the --install-dir CLI flag value (step 1).
3142// Set once at startup by main; never mutated thereafter.
3243var cliOverride string
3344
45+ // cliDisabled records that --install-dir= (explicit empty) was passed.
46+ // It is distinct from "cliOverride is empty" because the empty string
47+ // is also the "unset" sentinel for cliOverride itself. When set, Home()
48+ // returns "" so every on-disk consumer — filelog, ai-agent hook errors,
49+ // any future file derived from Home() — uniformly skips file output.
50+ var cliDisabled bool
51+
3452// SetOverride installs the CLI-flag value. Called by main after
3553// cli.Parse and before any code that calls Home() — see
3654// cmd/stepsecurity-dev-machine-guard/main.go.
3755func SetOverride (s string ) {
3856 cliOverride = s
57+ cliDisabled = false
58+ }
59+
60+ // SetDisabled marks the install dir as explicitly disabled for this
61+ // run. After this call Home() returns "" regardless of env/config/
62+ // legacy values, so no on-disk artifact is written under the resolved
63+ // install dir. Used by main when the operator passes --install-dir=
64+ // (empty) to silence per-run file logging.
65+ func SetDisabled () {
66+ cliDisabled = true
67+ cliOverride = ""
3968}
4069
4170// Home returns the resolved install dir. Falls back to LegacyHome when
42- // nothing else is set. Empty string is possible only when the home
43- // directory itself cannot be resolved. A leading $HOME or ~ token in
44- // any source is expanded via expandHome so the returned path is
45- // canonical for the current OS, keeping the migration warning in main
46- // from misfiring on hand-edited values like "$HOME/.stepsecurity" that
47- // resolve to the legacy default.
48- //
49- // Note: this is a superset of resolveSearchDirs in internal/scan/scanner.go,
50- // which only expands the exact literal "$HOME" — the install dir comes
51- // from operator-edited config so it has to tolerate the "$HOME/foo" /
52- // "~/foo" forms our docs use; search_dirs come from --search-dirs flag
53- // values that operators don't combine with subpaths.
71+ // nothing else is set. Returns "" when SetDisabled() was called or
72+ // when the home directory itself cannot be resolved. A leading $HOME
73+ // or ~ token in any source is expanded via expandHome so the returned
74+ // path is canonical for the current OS; the result is run through
75+ // filepath.Clean so trailing slashes and "." / ".." components don't
76+ // cause spurious mismatches in the migration-warning equality check.
5477func Home () string {
55- if cliOverride != "" {
56- return expandHome ( cliOverride )
78+ if cliDisabled {
79+ return ""
5780 }
58- if v := os .Getenv (HomeEnvVar ); v != "" {
59- return expandHome (v )
81+ // Read the env var once so a concurrent mutation (test helpers,
82+ // future goroutine setup) can't flip the value between the switch
83+ // guard and the assignment below — and so we don't pay for a second
84+ // syscall on every lookup.
85+ envHome := os .Getenv (HomeEnvVar )
86+ var raw string
87+ switch {
88+ case cliOverride != "" :
89+ raw = cliOverride
90+ case config .InstallDir != "" :
91+ raw = config .InstallDir
92+ case envHome != "" :
93+ raw = envHome
94+ default :
95+ return LegacyHome ()
6096 }
61- if config .InstallDir != "" {
62- return expandHome (config .InstallDir )
97+ resolved := expandHome (raw )
98+ if resolved == "" {
99+ return ""
63100 }
64- return LegacyHome ( )
101+ return filepath . Clean ( resolved )
65102}
66103
67104// expandHome replaces a leading $HOME or ~ token with the resolved
0 commit comments