A path is constructed exactly once. Everywhere else references the constructed value. This is the strict form of DRY for paths — paths drift the easiest because they're string literals that look harmless until two of them diverge and you spend an hour finding which copy is the source of truth.
- Within a package: every script imports its own
scripts/paths.mts. Nopath.join('build', mode, …)outside that module.paths.mtsis per-package (likepackage.json) — every package that has ascripts/dir has its own. - Across packages: package B imports package A's
paths.mtsvia the workspaceexportsfield. Neverpath.join(PKG, '..', '<sibling>', 'build', …). - Sub-packages inherit: a sub-package's
paths.mtsexport * from '<rel>/paths.mts'from the nearest ancestor and adds local overrides below the re-export. Don't re-deriveREPO_ROOT/CONFIG_DIR/NODE_MODULES_CACHE_DIR(enforced by.claude/hooks/paths-mts-inherit-guard/). - Not just build paths:
paths.mtsis for every path the package constructs — config files (socket-wheelhouse.json), lockfiles, cache dirs, manifest files. The fleet ships a startertemplate/scripts/paths.mtsthat exports the common constants +loadSocketWheelhouseConfig(). - Workflows / Dockerfiles / shell can't
importTS — construct once, reference by output /ENV/ variable.
Build outputs live at <package-root>/build/<mode>/<platform-arch>/out/Final/<artifact>, where mode ∈ {dev, prod} and platform-arch is the Node-style <process.platform>-<process.arch> (e.g. darwin-arm64, linux-x64). socket-btm is the worked example; ultrathink follows it; smaller TS-only repos that don't fork by platform may use 'any' as the platform-arch sentinel but keep the same nesting.
Each package's scripts/paths.mts exports at minimum:
PACKAGE_ROOT— absolute path to the package directoryBUILD_ROOT—<PACKAGE_ROOT>/buildgetBuildPaths(mode, platformArch)— returns at leastoutputFinalDir+outputFinalFileoroutputFinalBinary
| Level | Surface | What it catches |
|---|---|---|
| Edit-time | .claude/hooks/path-guard/ |
Build-path construction outside paths.mts |
| Edit-time | .claude/hooks/paths-mts-inherit-guard/ |
Sub-package paths.mts that doesn't inherit from the nearest ancestor |
| Commit-time | scripts/check-paths.mts (run by pnpm check) |
Whole-repo path-hygiene scan |
| Audit + fix | /guarding-paths skill |
Interactive cleanup |
- Recomputing a sibling's build dir. Import from the sibling's
paths.mtsinstead. - Hard-coding
build/dev/orbuild/prod/. UsegetBuildPaths(mode, ...)so a future--mode=stagingdoesn't require N edits. - Constructing the same
~/.socket/...cache dir in 3 places. Either it belongs inscripts/paths.mtsor in@socketsecurity/lib'spaths/module if it's truly cross-package.
When in doubt: find the canonical owner and import from it.