Repository navigation
[Bug]: systemd user unit from t3 service install sets no PATH, so providers in ~/.local/bin are invisible to the service #4913
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Not a duplicate, but same family as #4697 ("No installed editors found") and #4846 (Windows desktop resolver ignores User PATH). Those two plus this one all reduce to: the server resolves external commands against whatever PATH it inherited, and that PATH is wrong whenever it wasn't launched from an interactive login shell.
Area
apps/server
Steps to reproduce
On a headless Ubuntu box, with providers installed the ordinary way so their launchers land in ~/.local/bin:
command -v claude codex→/home/caesar/.local/bin/claude,/home/caesar/.local/bin/codexnpm i -g t3(npm prefix is~/.local/npm-global, no sudo needed)t3 service installsystemctl --user start t3code.service- Open the web UI and try to start a thread.
Expected behavior
The service resolves claude / codex exactly as t3 serve does when run by hand from a login shell.
Actual behavior
Providers are not found. The server itself is healthy — it listens, pairs, authenticates, and the web client works — but there are no usable providers, so no thread can run.
The cause is that the generated unit sets no PATH, so the service inherits the systemd user manager's minimal environment:
- Service PATH (what
t3gets):/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin - Login shell PATH (what a human gets):
/home/caesar/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
~/.local/bin is prepended by Ubuntu's default ~/.profile, which only runs for login shells. A systemd user unit never sources it. So the one directory where user-scoped CLI tools normally live is exactly the one the service cannot see.
This also makes the failure confusing to diagnose: running t3 serve manually over SSH works if you have a login shell, so the provider list looks fine when you test by hand and empty when the service runs.
Impact
Major degradation or frequent failure
The headless Linux path is the "self-hosted server" use case, and on a fresh install it silently produces a server with zero providers.
Version or commit
t3@0.0.31
Environment
Ubuntu 26.04 LTS, kernel 7.0.0-28-generic, x86-64 · node v22.22.1 at /usr/bin/node · npm prefix /home/caesar/.local/npm-global · systemd user service with Linger=yes · no desktop session, headless, reached over Tailscale.
Logs or stack traces
# unit generated by `t3 service install` (verbatim) - note the absence of any PATH
[Unit]
Description=T3 Code server
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
WorkingDirectory=%h
Environment=T3CODE_HOME=/home/caesar/.t3
Environment=T3_BOOT_SERVICE_UNIT=t3code.service
ExecStart=/usr/bin/node /home/caesar/.local/npm-global/bin/t3 serve
Restart=always
RestartSec=5
StandardOutput=append:/home/caesar/.t3/userdata/logs/boot-service.log
StandardError=append:/home/caesar/.t3/userdata/logs/boot-service.log
[Install]
WantedBy=default.target
# what the running service actually has, before the workaround
$ tr '\0' '\n' < /proc/$(systemctl --user show t3code.service -p MainPID --value)/environ | grep ^PATH=
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
# where the providers actually are
$ bash -lc 'command -v claude codex'
/home/caesar/.local/bin/claude
/home/caesar/.local/bin/codexWorkaround
A systemd drop-in at ~/.config/systemd/user/t3code.service.d/override.conf, which also survives t3 service update (editing the generated unit does not):
[Service]
Environment=PATH=/home/caesar/.local/bin:/home/caesar/.local/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binAfter systemctl --user daemon-reload && systemctl --user restart t3code.service, providers resolve and threads run normally. Note that systemctl --user enable --now does not restart an already-active unit, so a plain enable --now after editing the drop-in silently keeps the old environment.
Suggested fix
Have t3 service install emit an explicit Environment=PATH= in the unit, captured from the installing user's login PATH (bash -lc 'echo $PATH') rather than from the environment the installer happens to run in. At minimum, prepend ~/.local/bin and the npm global bin directory.
Doing it at install time also fixes #4697-style reports on Linux, and is the same underlying defect as #4846 on Windows.
Related papercut (secondary, happy to split out)
t3 service install exposes no --host / --port, and the unit it writes binds to loopback. On a headless box that means the freshly installed service is unreachable from anywhere until you discover that T3CODE_HOST / T3CODE_PORT work as environment variables. I verified both do, via the same drop-in:
Environment=T3CODE_HOST=100.x.x.x
Environment=T3CODE_PORT=3773Either exposing flags on service install or documenting those variables in the headless setup path would save the guesswork.
Activity
martinfrancois commentedon Aug 5, 2026
Before someone adds
Environment=PATH=to the unit, two things worth checking./proc/<pid>/environis the environment the process started with, and does not change when theprocess edits
process.env. The server edits it:fixPath()inapps/server/src/os-jank.tsruns unconditionally at startup, spawns
$SHELL -ilc, and merges the login shell'sPATHin.That code is in 0.0.31, not just
main.On my host it recovers the directory, starting from exactly the service's exec-time environment:
So
~/.local/binis on thePATHthe server resolves against, even though the unit never setit. If the providers were confirmed missing from the UI or from a spawn error in
boot-service.lograther than from thePATHcomparison, then the cause is probably elsewhereand this issue's title points at the wrong thing.
Second, the obvious fix is a trap. A unit-level
Environment=PATH=replaces the systemd usermanager environment for that variable rather than adding to it, so anything arriving via
~/.config/environment.d/*.conforsystemctl --user import-environmentis dropped. On Ubuntuthat is how
/snap/binreaches user services, so a snap-installed tool that works today wouldgo missing after the next
t3 service update.Workaround for anyone who is genuinely affected, so nobody has to wait on this. A drop-in
survives
t3 service updaterewriting the unit, unlike editing the unit itself:Check the drop-in applied.
/proc/environis the right tool for this one, since a drop-in setsthe exec-time environment, which is exactly what that file shows. The
PATHthe server ends upresolving against may be longer, because
fixPath()still merges the login shell's entries ontop of it at startup:
Spell out the full value you want, including the system directories, for the
replace-not-extend reason above. Keep the quotes: systemd expands
%hbefore splitting theline on whitespace, so an unquoted value truncates for anyone whose home directory contains a
space.
AI Disclosure: Prepared with the assistance of Claude Code (Claude Opus 5). The commands and
output above were run and observed on my own machine. Reviewed by me before posting.
BJSchmidt commentedon Sep 23, 2026
Confirming a variant of this on
t3@0.0.42, wherefixPath()does not help: the T3 Connect relay client (cloudflared) in~/.local/binis invisible to the systemd service, so the managed tunnel never starts.t3 connectgives no hint.What happened
npx t3 serveto the self-contained install, then rant3 service install.t3 connectonly said "T3 Code is already set up to run in the background on this machine."t3 connect statusreportedEnvironment link: provisionedandRelay client: available via PATH(~/.local/bin/cloudflared, 2026.5.2, the pinned version).cloudflaredprocess was running.An earlier boot, with the same binary present, logged the underlying reason:
The service was restarted several times, each less than 10 minutes apart, so for a while nothing was logged at all.
Why
fixPath()doesn't cover thisThe login shell does include
~/.local/bin(bash -ilc 'echo $PATH'confirms it), andfixPath()runs; itsbash: cannot set terminal process groupnoise is in the log. However, the relay client doesn't readprocess.env.PATHdirectly. It resolvescloudflaredthroughConfig.string("PATH")(packages/shared/src/relayClient.ts:117-118). In the bundled effect-smol,ConfigProvider.fromEnv()copiesprocess.envwhen the provider is constructed ({ ...process.env }). If the default provider was first used beforefixPath()ran, it still holds the unhydrated PATH. Our reading of the code suggests that is what happens; we didn't verify it at runtime. Either way, the observed result is that providers may resolve through the hydratedprocess.env, whilecloudflaredresolution sees the systemd PATH.t3 connectchecks resolution from the interactive shell, seescloudflaredon PATH, and never installs the managed copy under<T3CODE_HOME>/tools/cloudflared/…. So there is no fallback.Workaround (verified)
Point the service at the binary explicitly with the existing override variable, via a drop-in:
Within a second of the restart, the service logged
Relay client process started,T3 Connect desired link reconciled on startupandRelay client tunnel connection registered, and remote connection from another machine works. This sidesteps theEnvironment=PATH=replace-not-extend concern raised above, because it doesn't touch PATH.Suggestions
cloudflaredagainst the hydrated PATH, for example by readingprocess.env.PATHafterfixPath(), or by havingfixPath()run before anyConfigProvideris built.t3 connect/t3 service installfindscloudflaredonly via the user's PATH, persist it asT3CODE_CLOUDFLARED_PATHin the unit, or install the managed copy.t3 connect statusandt3 connect(today they report "provisioned" or "already set up" while the tunnel is down), and log the first reconcile failure right away rather than after 10 minutes.Environment
~/.t3/runtime/versions/0.0.42/t3), systemd user service viat3 service install~/.local/bin/cloudflared/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/binDiagnosed and drafted via
t3 triagewith Claude Code (Claude Opus 5.5); reviewed by the reporter before posting.valshin commentedon Sep 24, 2026
Confirming on
t3@0.0.42, Ubuntu 24.04 x64, headless, systemd user service (Linger=yes). Same symptoms as the comment above:cloudflaredin~/.local/bin, service exec PATH lacks it,environment.cloud.applyRelayConfigfails withManaged endpoint runtime could not be started., and theT3CODE_CLOUDFLARED_PATHdrop-in fixed it immediately (tunnel registered within ~1s of restart).One extra detail:
t3 connect statusreportsVersion: 2026.5.2for the PATH-resolved binary, but that binary is actuallycloudflared 2026.8.2. The relay client'sresolvereturns the pinnedCLOUDFLARED_VERSIONfor every source (managed,path,override) rather than the version of the executable it found, so the status output is misleading about the version too, not just about the tunnel being up.