The symlink existed because the Claude sidecar hardcoded that path, and that
hardcoding came from the bwrap-sandboxed architecture: the jail ro-bound /usr
and could not see the installer's real target in ~/.local/bin. The sandbox is
gone, and claude-manager.ts now resolves the CLI itself — $CLAUDE_BIN, then
PATH, then ~/.local/bin/claude, /usr/local/bin/claude, /opt/homebrew/bin/claude.
Verified before removing rather than assumed:
- the only references left in the tree are the resolver's own fallback list and
this step; nothing in capabilities, no systemd unit, no crontab, no ecosystem
file and no shell rc mentions the path
- the agent sidecar's PATH under pm2 contains ~/.local/bin ahead of
/usr/local/bin, so Bun.which resolves to the installer's target and the
symlink is never consulted
- replaying the resolver in that exact environment with the symlink treated as
absent returns the same path, so it is not load-bearing
- resolveClaudeBin runs at claude-manager module scope, which ES import ordering
puts before user-instance.ts reassigns process.env.HOME — so the homedir()
candidate is evaluated against the real home, not the managed one
The install-and-verify step above is untouched, so a failed claude-code install
is still reported. Only the sudo-owned link into /usr/local/bin goes, a
directory macOS does not ship at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The setup wrote /etc/sudoers.d/officer-service with tee and then chmod'd it.
Two problems, both with the same worst case: a malformed or wrongly-permissioned
file there breaks sudo completely, and you cannot sudo to repair it — on a
remote machine that means physical access or a rescue boot.
Generate into a temp file, gate on `visudo -c`, and only then install. Use
install(1) rather than tee+chmod so the content and the 0440 mode land in one
step; tee creates at the default umask first, and sudo refuses to read a sudoers
file with loose permissions, so the old ordering had a window where sudo could
reject its own configuration.
The re-run guard also grepped for the username anywhere in the file, so a
comment mentioning it counted as configured. Match the actual rule instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two guards that checked something other than the state they were protecting.
Section 17 skipped the entire remote desktop setup when `dpkg -s ubuntu-desktop`
succeeded, treating one package being present as proof that seven steps of
configuration had run. A host can have ubuntu-desktop and still be missing GDM
auto-login, the forced Xorg session, the captured EDID and its kernel command
line, and the login-time mode setter — which is exactly what this machine was
on 2026-08-02, while the guard cheerfully reported "skip". setup-desktop.sh is
idempotent throughout, so the guard bought nothing and cost a converged host.
The starship step had the opposite bug: it cp'd over ~/.config/starship.toml on
every run, so a customised config was silently destroyed. The nvim step two
sections down already guards on its config's existence; this now matches, and
distinguishes "absent" (deploy) from "identical" (skip) from "yours differs"
(keep, and say how to take ours).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 17 was still labelled "XFCE + VNC" while the step it runs installs
ubuntu-desktop and is guarded on it, so the heading described a setup the
script had already stopped producing.
Also spell out why setup-desktop.sh disables lightdm: it is not a display
manager this script ever installs, it is residue on hosts set up by an earlier
version that did install XFCE, and left enabled it beats GDM to the seat.
Comments and one echo string; no behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setup.sh installed pm2 but never ran anything with it, so a fresh install
finished with every dependency in place and nothing listening. That is not
cosmetic: /desktop returns 503 until officer-vnc is connected, and chat needs
officer-claude.
Adds a step that runs `pm2 startOrRestart ecosystem.config.cjs`, saves the
process list, and enables the boot unit when it is not already there. Using
startOrRestart rather than start means apps added to the ecosystem since the last
run get picked up — officer-music is in the ecosystem on this box but was never
running, for exactly that reason.
The verification block now reports which services are up, with the names read
from ecosystem.config.cjs so the list cannot drift as sidecars are added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fresh clone has neither: index.gen.html is gitignored and built from
PUBLIC_URL, and the database schema is applied with push rather than migrations.
Without both, setup finishes on a checkout that cannot serve a page or reach a
table.
Runs after .env is written, since both depend on it. Failures warn rather than
abort so the rest of the verification still reports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Claude sidecar execs /usr/local/bin/claude (claude-manager.ts), but the
Anthropic installer only puts the CLI in ~/.local/bin — so on a fresh host that
path doesn't exist and claude chat fails with
"ENOENT … posix_spawn '/usr/local/bin/claude'". Symlink ~/.local/bin/claude →
/usr/local/bin/claude after install (idempotent; tracks Claude's self-updates).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The item store location wasn't written to .env, so a fresh server fell back to
<repo>/officer-items and booted with an empty store. Prompt for it (default: a
sibling of the repo) and write it to .env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the
Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an
Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM
per-session path (/run/user/<uid>/gdm/Xauthority) with a ~/.Xauthority fallback.
setup.sh fixes:
- desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned
officer-vnc service, which never matched so setup-desktop re-ran every time)
- remove Pi (install, --list-models validation, verification check)
- export GOPATH before the cliamp build so `go install` lands where it's checked
even when Go was already present this run
- write PUBLIC_BUILD_ENV=production and quote all .env values
- guard the interactive .env block behind a TTY check so non-interactive runs
skip cleanly instead of aborting on read EOF under set -e
- restart systemd-logind only when a key actually changed
- sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move claude and pi from sudo global installs to ~/.local. Claude
binary is copied to /usr/local/bin for sandbox visibility, pi runs
via node from ~/.local/lib (ro-mounted). Fix bwrap intermediate dir
traversal by setting 0755 perms on auto-created HOME dirs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- chown user dirs to pastilhas:<username> instead of pastilhas:officerdev
so users cannot access each other's data
- chmod 2770 (setgid) gives only the owning user terminal access
- setup.sh: ensure home dir is traversable (o+x) for provisioned users
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- provision linux users with pastilhas:officerdev ownership so server
can always read/write, terminal users get group access
- add officerdev shared group setup to setup.sh
- move go install to ~/.local/go with GOPATH at ~/.local/go-path
- add upload file/folder items to file browser context menu
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add isync to setup.sh
- ask for gmail address alongside app password in integrations
- add PermanentError to job queue (skips retries for non-recoverable failures)
- use PermanentError for missing credentials, missing executable, auth failures
- auto-add /email to dock after successful gmail sync
- invalidate dock cache on sync completion for seamless UI update
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mask sleep.target, suspend.target, hibernate.target, hybrid-sleep.target
- Configure systemd-logind to:
- Ignore lid switch events
- Ignore power key presses
- Disable idle action
- Set runtime directory size
- Restart systemd-logind to apply changes
Servers running Officer shouldn't go to sleep when idle.
This prevents unexpected suspends during setup or normal operation.
- Set npm prefix to /usr/local (system location) instead of user-specific ~/.npm-global
- Ensures all users can access installed npm packages
- Fixes issue where packages installed to one user's home directory
- All npm global packages now available system-wide to all users
This ensures that after running setup.sh:
- pi, claude, and other tools are at /usr/local/bin/
- Accessible to all users (pastilhas, andrepadez, etc.)
- New users automatically get access too
- When installing global npm packages, explicitly use /usr/bin/npm
- Falls back to $(which npm) if system npm not found
- Ensures packages go to system location, not nvm location
- Works around nvm PATH interference during setup
- setup.sh now automatically installs Node 22 via NodeSource if not found
- No more separate manual Node installation step required
- Simplified to single command: bash scripts/setup.sh
- Works for apt/pacman/brew systems
- Updated SETUP_GUIDE.md with simplified instructions
- Better logging during Node installation process
Users can now simply run:
bash scripts/setup.sh
And everything (including Node 22) will be installed automatically.
- Added detailed error logging to detect snap node compatibility issues
- When Pi process exits with code 1, log helpful diagnostic info including node path
- Add hint to check for snap node and reinstall via apt/nvm
- Create SNAP_NODE_COMPATIBILITY.md with full troubleshooting guide
- Document root cause: snap node has file descriptor incompatibility with Bun.spawn stdin pipes
- Provide clear installation instructions for NodeSource and nvm alternatives