Skip to content

Install trail without sudo - #734

Merged
anirudhkumar-nanonets merged 2 commits into
mainfrom
claude/install-without-sudo-9b8267
Oct 29, 2026
Merged

anirudhkumar-nanonets merged 2 commits into
mainfrom
claude/install-without-sudo-9b8267

Conversation

@anirudhkumar-nanonets

@anirudhkumar-nanonets anirudhkumar-nanonets commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Issue: At least four testers couldn't install or upgrade trail: npm asked for sudo, reinstalls failed with ENOTEMPTY or EEXIST, and an old Node broke setup without saying so.
Why: On a Node from nodejs.org only root can write npm's global folder; a stopped npm install leaves a folder every later install trips on; and trail said it needed Node 20 when it really needs 22.3 (or 20.16).
Fix: trail upgrade now clears npm's leftovers and tries again, and turns EACCES into a no-sudo fix; trail stops on an old Node with one plain line; the README, the trail skill and the session hook show the no-sudo setup.

Moves: installs that reach setup, install vs init_completed per install id over the last 7 days (PostHog, Metabase db 11, properties->>'cli_name' = 'trail'): 478 of 1,516 install ids (31.5%) on 2026-10-29.

What changed

  • trail upgrade / graft upgrade (runUpgrade, src/cli-meta.ts): npm's stderr is kept so its npm error code/path/dest lines can be read.
    • ENOTEMPTY: npm moves the old package aside to .<name>-<hash of its path> before writing the new one. The name is the same every run, so one stopped install breaks every later one. Trail removes that folder (only when it's npm's dot-named sibling of the package) and tries again.
    • EEXIST: a trail/graft link in npm's bin folder from another copy of trail. Removed and retried only when it points into a trail or graft install, or at nothing. Any other file is left alone.
    • EACCES: instead of npm's stack trace, a plain fix: an npm prefix in the home folder (~/.npm-global), with the PATH line for the user's shell (zsh, bash, fish). No sudo.
    • Anything else: npm's own error, as before. At most two retries.
  • Node check (src/node-version.ts, both bins): trail and graft check Node before anything else loads (everything after is a dynamic import). engines is now ^20.16.0 || >=22.3.0, where process.getBuiltinModule exists; on Node 18, trail init "worked" but couldn't read past sessions.
  • README: a tip under the quick-start install line with the no-sudo setup and the Node floor.
  • Skill and session hook (src/claude/skill-template.ts, src/claude/shim-template.ts): where they tell a teammate's Claude to run npm install -g @trailhq/trail-code, they now add: on EACCES, never use sudo (Claude can't type the password anyway); offer to point npm at ~/.npm-global and add it to PATH, then install again. One short clause in the skill description, which every teammate's Claude sees; the steps in the skill body and the hook. Wired repos pick up the new text at their next session.

Choices I made

  • Node floor 22.3 / 20.16, not 22. Trail's own code needs process.getBuiltinModule (22.3, backported to 20.16). This week 50 install ids ran Node 20; a flat >=22 would have cut off the ones on 20.16+, for whom trail works. 8 ran Node 18, 1 Node 16.
  • No Node message in postinstall. npm 7+ hides a successful lifecycle script's output: nobody would see it (the existing "Trail installed" nudge never shows on npm i -g either). The bin is the check that reaches people; npm's own EBADENGINE warning now names the right range.
  • A plain npm i -g that fails with EACCES fails before any trail code runs, so the fix for first installs is the README tip. trail upgrade prints the same fix.
  • Never suggest sudo: a sudo install makes every upgrade need sudo, and leaves root-owned files in the user's home (see follow-ups).

Tests

  • New: test/node-version.test.ts; test/cli-meta.test.ts covers parsing npm's error, both leftovers (and what's left alone), the no-sudo fix per shell, and the report.
  • Each failure was reproduced for real in its own scratch npm prefix: trail 0.27.0 from npm (before) and a tarball of this branch (after).
  • npm test: the full suite ran while the reproductions were installing packages, and 36 hook and journey tests failed on timing. Re-run alone, those 22 files give the same result on origin/main and on this branch: 159 pass, 5 fail, the same 5. They fail on origin/main on this machine too, because they depend on what's installed here, such as a trail on PATH; CI runs them on a clean machine.
  • After the second commit (skill and hook text), the eight related test files pass, apart from 2 of those same 5.
  • End to end on the final build (b3c11e9): both packages packed from it and installed into fresh scratch npm folders and homes, on Node 22.23.4 and 18.20.8. 23 of 23 checks pass: install and trail init (the skill and hook it writes carry the no-sudo line), Node 18 stopping trail and graft with nothing written, a plain trail upgrade, EACCES, ENOTEMPTY, a stale trail link, someone else's trail file left alone, two leftovers in one run, and an unreachable registry showing npm's own error. Not covered: a real sudo install, Windows, and a live Claude session following the new hook line.
  • CI: Ubuntu tests pass. Windows failed once, on one timing-sensitive learned-skills prompt hook test (test/skills-prompt.test.ts, D17) that this PR doesn't touch. It passes locally, and main's own Windows runs today fail in the same family of hook tests; the job is being re-run.
  • trail check: one flagged conflict, a false match. src/claude/skill-template.ts "brings back home trail never" from the Oct 27 learning about tests run with the real HOME (the shim stood down for a copy Claude never loads). The new line only shares those words: it tells Claude to set npm's prefix to a folder in the person's home, and has nothing to do with HOME in tests.

Follow-ups (not in this PR)

  1. The website's install box (trail-website repo) needs the same no-sudo line.
  2. After sudo npm i -g, postinstall writes ~/.graft/telemetry.json as root (macOS keeps HOME under sudo). The user's own trail can't read it and makes a new install id each run, which splits install from init_completed in this metric. Postinstall could hand the folder back to SUDO_UID.
  3. The "Trail installed. Run trail init" postinstall line is never shown on a global install (npm 7+ hides it).
  4. Windows: EACCES/EPERM still shows npm's own error.

🤖 Generated with Claude Code

@trailhq-graft

trailhq-graft Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 4 areas can be affected. 16 dependent symbols, depth 2.
Tests: 1 area updated its tests.
Tag: @shhdwi — 6 of 7 areas · @Frankie-Xu — 3 of 7 areas · @xiehuanyi — Claude Initialization

flowchart TB
  A0(("Host Management<br/>10 symbols"))
  A1(("CLI Upkeep<br/>3 symbols"))
  A2(("Claude Initialization<br/>2 symbols"))
  A3(("Install Paths<br/>1 symbol"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3 reached;
Loading
Can be affected Symbols Nearest hop Reached from
Host Management 10 src/hosts/antigravity.ts:L1-L62 antigravity.ts — imports, depth 1 Template Definitions
CLI Upkeep 3 src/cli.ts:L1-L2844 cli.ts — calls, depth 1 Template Definitions, CLI Metadata
Claude Initialization 2 src/claude/init.ts:L1-L305 init.ts — imports, depth 1 Template Definitions
Install Paths 1 src/init/install-paths.ts:L1-L84 install-paths.ts — imports, depth 2 Template Definitions
Who knows this code — 3 people across 7 areas
Area Who knows it
Template Definitions · changed @shhdwi — 3 commits, last 2d ago · @Frankie-Xu — 1 commit, last 7d ago
CLI Metadata · changed @shhdwi — 1 commit, last 6d ago · @Frankie-Xu — 1 commit, last 7d ago
Binary Operations · changed @Frankie-Xu — 1 commit, last 7d ago
Host Management · affected @shhdwi — 6 commits, last 2d ago
CLI Upkeep · affected @shhdwi — 26 commits, last 2d ago
Claude Initialization · affected @shhdwi — 3 commits, last 2d ago · @xiehuanyi — 1 commit, last 7d ago
Install Paths · affected @shhdwi — 1 commit, last 6d ago

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 16 dependent symbols, grouped by area

Host Management — 10 symbols in 10 files

  • src/hosts/antigravity.ts:L1-L62 — antigravity.ts (imports, depth 1)
  • src/hosts/claude-global.ts:L1-L278 — claude-global.ts (imports, depth 1)
  • src/hosts/codex-hooks.ts:L1-L165 — codex-hooks.ts (imports, depth 1)
  • src/hosts/cursor-hooks.ts:L1-L117 — cursor-hooks.ts (imports, depth 1)
  • src/hosts/init.ts:L1-L124 — init.ts (imports, depth 1)
  • src/hosts/instructions.ts:L1-L83 — instructions.ts (imports, depth 1)
  • src/hosts/registry.ts:L1-L186 — registry.ts (imports, depth 1)
  • src/hosts/auto.ts:L1-L155 — auto.ts (imports, depth 2)
  • src/hosts/plan.ts:L1-L101 — plan.ts (imports, depth 2)
  • src/hosts/retract.ts:L1-L545 — retract.ts (imports, depth 2)

CLI Upkeep — 3 symbols in 3 files

  • src/cli.ts:L1-L2844 — cli.ts (calls, depth 1)
  • src/upkeep-run.ts:L1-L103 — upkeep-run.ts (imports, depth 2)
  • src/upkeep.ts:L1-L326 — upkeep.ts (imports, depth 2)

Claude Initialization — 2 symbols in 2 files

  • src/claude/init.ts:L1-L305 — init.ts (imports, depth 1)
  • src/claude/hooks.ts:L1-L580 — hooks.ts (imports, depth 2)

Install Paths — 1 symbol in 1 file

  • src/init/install-paths.ts:L1-L84 — install-paths.ts (imports, depth 2)
Test signal per changed area — 1 ✓ · 2 –

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • – Template Definitions — 1 test file changed here: test/claude-shim-resolve.test.ts
  • ✓ CLI Metadata — 5 of 9 reached · 2 test files changed here: test/cli-meta.test.ts, test/node-version.test.ts
    • not reached: isTrailLink, npmRun, field, runUpgrade
  • – Binary Operations — no function, method or class changed here
33 test suites also reference this code

33 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/claude-hook-layers.test.ts
  • test/claude-hook-timeouts.test.ts
  • test/claude-hooks-fail-open-all.test.ts
  • test/claude-host-standdown.test.ts
  • test/claude-init.test.ts
  • test/claude-pre-bash-fail-open.test.ts
  • test/claude-pre-bash-wiring.test.ts
  • test/claude-pre-bash.test.ts
  • test/claude-settings-merge.test.ts
  • test/claude-shim-template.test.ts
  • test/claude-skill-template.test.ts
  • test/claude-start-skills-2.test.ts
  • test/cli-status-codex.test.ts
  • test/graft-to-trail.test.ts
  • test/hosts-antigravity.test.ts
  • test/hosts-auto.test.ts
  • test/hosts-claude-global.test.ts
  • test/hosts-codex-hooks-2.test.ts
  • test/hosts-codex-hooks.test.ts
  • test/hosts-codex-trust.test.ts
  • …13 more

⚠️ 5 changed files not in the graph (.trail/learnings/2026-10-29-trail-upgrade-recovers-npm-s-eacces-anirudhkumar.md, README.md, package-lock.json, package.json, test/fixtures/skills/trail/SKILL.md) — no parser claims the extension, or the index predates the file.

graft blast · refs/graft/base...HEAD · depth 2 · 15 changed files

Open the interactive graph → — click an area to see the code that changed, and the line that reaches it.

github-actions Bot added a commit that referenced this pull request Oct 29, 2026
github-actions Bot added a commit that referenced this pull request Oct 29, 2026
@anirudhkumar-nanonets
anirudhkumar-nanonets merged commit 55b03ae into main Oct 29, 2026
10 of 11 checks passed
github-actions Bot added a commit that referenced this pull request Oct 29, 2026
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.

1 participant