Goose Desktop Environment Fix
Fix the Goose Desktop shell tool so that shell commands run with the
full user environment — including devbox/nix-installed tools, crc/oc,
cargo, sdkman, and custom exports.
Problem
Goose Desktop’s shell tool runs non-interactive, non-login bash
shells (bash -c "<command>"). Bash only sources ~/.bashrc for
interactive shells, so all user environment setup is missing:
| Shell Type | Files Sourced |
|---|---|
| Login + Interactive | ~/.bash_profile → ~/.bashrc |
| Interactive (non-login) | ~/.bashrc |
| Non-interactive, non-login | Only $BASH_ENV (if set) |
Goose Desktop falls into the third category, resulting in:
- Bare
PATH(missing devbox/nix, cargo, crc/oc, sdkman paths) - Missing environment variables
- Tools like
gh,oc,hey,javanot found
Additionally, Goose Desktop on Fedora/GNOME may have a user-level
.desktop file override at ~/.local/share/applications/Goose.desktop
that hardcodes PATH via Exec=env PATH=..., preventing
environment.d variables from reaching the Goose process.
Root Causes
- Bash sourcing rules — non-interactive shells don’t source
~/.bashrc - Nix double-source guard —
nix-daemon.shsets__ETC_PROFILE_NIX_SOURCED=1and skips if already set. The GNOME session sets this during login, but the Goose.desktopfile overridesPATHwithout nix paths, so nix is “sourced” but its paths are missing. - Desktop file override — the user-level
.desktopfile’sExec=env PATH=...line overrides session environment variables set via~/.config/environment.d/ - Devbox
shellenvrecursive fork bomb —devbox global shellenvspawns a bash subprocess to compute the environment. That subprocess sources~/.bashrc(viaBASH_ENVor thegoose-shellwrapper), which callsdevbox global shellenvagain, creating an infinite recursion. This rapidly spawns thousands of processes (observed: 5,763+), consuming CPU and risking system instability.
Devbox Fork Bomb — Detailed Analysis
Trigger Chain
Goose Desktop
→ goose-shell (sources ~/.bashrc)
→ .bashrc: eval "$(devbox global shellenv)"
→ devbox spawns bash subprocess
→ bash sources ~/.bashrc (via goose-shell or BASH_ENV)
→ .bashrc: eval "$(devbox global shellenv)" ← RECURSION
→ devbox spawns bash subprocess
→ ... (infinite loop, 5,763+ processes observed)
Symptoms
- Thousands of
devbox global shellenvprocesses visible inps aux - Each process is a child of the previous, forming a deep nesting chain
- CPU load rises (each process uses a small amount, but thousands add up)
pstreeshows a deeply nested chain:systemd → bash → devbox → devbox → devbox → ... (thousands deep)
Emergency Cleanup
If the fork bomb is already running:
pkill -f 'devbox global shellenv'
Prevention: Shell-Local Guard
Add a recursion guard in ~/.bashrc around the devbox global shellenv
call. This is the critical fix — without it, the fork bomb will
recur on every Goose Desktop restart.
v1.2 pattern — uses a shell-local (non-exported) variable:
# Shell-local guard: prevents re-eval within the same shell process, but does NOT
# propagate to child shells (e.g. devbox shell), so they get a fresh eval and the
# refresh-global alias is properly created. Safe against fork bombs because
# devbox global shellenv just prints text — it doesn't spawn a shell.
if [ -z "$__devbox_shellenv_done" ]; then
__devbox_shellenv_done=1
eval "$(devbox global shellenv 2>/dev/null)"
fi
Why shell-local, not exported?
- An exported guard (like the old
export __DEVBOX_SHELLENV_LOADED=1) is inherited by child shell processes such asdevbox shell. - When
devbox shellstarts a new bash, that bash sources~/.bashrc, sees the inherited guard, and skipsdevbox global shellenventirely. - This means the
refresh-globalalias is never created indevbox shell. - A shell-local variable (
__devbox_shellenv_done=1withoutexport) prevents re-eval within the same process but does NOT propagate to child processes, sodevbox shellgets a fresh eval and the alias works. - Fork bomb safety is maintained because
devbox global shellenvjust prints text to stdout — it does not spawn a shell. Theevalevaluates that text in the current shell. Additionally, devbox’s own output includesexport __DEVBOX_SHELLENV_LOADED=1, providing a secondary layer of protection.
Why __GOOSE_SHELL_SOURCED alone is not enough: The goose-shell
wrapper’s own guard (__GOOSE_SHELL_SOURCED) prevents re-sourcing
~/.bashrc from the wrapper level, but it does NOT prevent the
internal recursion caused by devbox global shellenv spawning child
bash processes that re-enter .bashrc through a different code path.
The shell-local guard catches this inner recursion.
Solution (3 files)
The fix has three components that work together:
File 1: ~/.local/bin/goose-shell (wrapper script)
A bash wrapper that sources ~/.bashrc before executing the command.
Goose calls $GOOSE_SHELL -c "<command>", so this wrapper runs first.
#!/bin/bash
# Goose Desktop shell wrapper — sources ~/.bashrc for full environment
# before executing the command passed via -c.
# Guard against recursive sourcing when exec'd bash also triggers this wrapper.
if [ -z "$__GOOSE_SHELL_SOURCED" ]; then
export __GOOSE_SHELL_SOURCED=1
# The Goose .desktop file sets a bare PATH, so nix/profile.d paths
# are missing. Clear the nix "already sourced" guard so nix-daemon.sh
# re-adds its paths.
unset __ETC_PROFILE_NIX_SOURCED
unset BASHRCSOURCED
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
exec /bin/bash "$@"
Key details:
__GOOSE_SHELL_SOURCEDguard prevents infinite recursionunset __ETC_PROFILE_NIX_SOURCEDforcesnix-daemon.shto re-add nix paths (they were lost when the.desktopfile overrodePATH)unset BASHRCSOURCEDlets/etc/bashrcre-run to source/etc/profile.d/*.shscriptsexec /bin/bash "$@"replaces the wrapper with real bash to run the actual command
File 2: ~/.local/share/applications/Goose.desktop (desktop entry)
The user-level .desktop file must pass GOOSE_SHELL to the Goose
process. Add it to the existing Exec=env ... line:
[Desktop Entry]
Name=Goose
Exec=env GOOSE_SHELL=$HOME/.local/bin/goose-shell PATH=$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin /usr/lib/Goose/Goose %U
Icon=/usr/share/pixmaps/Goose.png
Terminal=false
Type=Application
Categories=Development;
MimeType=x-scheme-handler/goose;
Note: Replace $HOME with the actual home directory path (e.g.,
/home/username) since .desktop files don’t expand shell variables.
If there is no user-level .desktop override, you can either:
- Create one from the system file at
/usr/share/applications/Goose.desktop - Or rely on
environment.dalone (File 3)
File 3: ~/.config/environment.d/60-goose-shell.conf
Sets GOOSE_SHELL in the systemd user environment as a fallback for
any launch method that doesn’t use the .desktop file:
# Tell Goose Desktop to use our wrapper shell that sources ~/.bashrc
GOOSE_SHELL=$HOME/.local/bin/goose-shell
Note: Replace $HOME with the actual home directory path.
Prerequisite: ~/.bashrc restructuring
The ~/.bashrc must be restructured with an interactive guard so
that environment setup runs for all shells, but interactive-only setup
(completions, prompt integration) is skipped for non-interactive shells:
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# ============================================================
# Environment setup (runs for ALL shells)
# ============================================================
# PATH additions
if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]; then
PATH="$HOME/.local/bin:$HOME/bin:$PATH"
fi
export PATH=$PATH:$HOME/.cargo/bin
# User-specific aliases and functions
if [ -d ~/.bashrc.d ]; then
for rc in ~/.bashrc.d/*; do
[ -f "$rc" ] && . "$rc"
done
fi
unset rc
# Devbox environment
# Shell-local guard: prevents re-eval within same process, but does NOT
# propagate to child shells (e.g. devbox shell), so they get a fresh
# eval and the refresh-global alias is properly created.
if [ -z "$__devbox_shellenv_done" ]; then
__devbox_shellenv_done=1
eval "$(devbox global shellenv 2>/dev/null)"
fi
# Other environment setup (crc/oc, exports, sdkman, etc.)
# ... add your environment exports here ...
# SDKMAN — must be near the end of the environment section
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
# ============================================================
# Interactive-only setup (completions, prompt integration)
# Skipped for non-interactive shells (Goose Desktop, scripts, cron)
# ============================================================
[[ $- != *i* ]] && return
# Goose terminal integration
eval "$(goose term init bash)"
# Completions (devbox, hermes, gcloud, etc.)
. <(devbox completion bash 2>/dev/null)
eval "$(hermes completion bash 2>/dev/null)"
Key principle: Everything above [[ $- != *i* ]] && return runs
for ALL shells. Everything below runs only in interactive terminals.
Caution with crc oc-env: The output of crc oc-env contains
a comment line with an unmatched single quote (# eval $(crc oc-env)).
Using eval "$(crc oc-env)" in non-interactive shells causes a
parsing error. Replace it with a direct export PATH= statement:
# Instead of: eval "$(crc oc-env 2>/dev/null)"
export PATH="$HOME/.crc/bin/oc:$PATH"
Guard Variables — Summary
| Variable | Scope | Set By | Purpose |
|---|---|---|---|
__GOOSE_SHELL_SOURCED |
Exported | goose-shell wrapper |
Prevents ~/.bashrc from being re-sourced at the wrapper level |
__devbox_shellenv_done |
Shell-local (NOT exported) | ~/.bashrc |
Prevents devbox shellenv re-eval within same process; does NOT propagate to child shells so devbox shell gets refresh-global |
__DEVBOX_SHELLENV_LOADED |
Exported | devbox’s own output | Secondary safety net — devbox’s built-in recursion guard |
Steps to Apply
Step 1: Create the goose-shell wrapper
cat > ~/.local/bin/goose-shell << 'EOF'
#!/bin/bash
if [ -z "$__GOOSE_SHELL_SOURCED" ]; then
export __GOOSE_SHELL_SOURCED=1
unset __ETC_PROFILE_NIX_SOURCED
unset BASHRCSOURCED
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
exec /bin/bash "$@"
EOF
chmod +x ~/.local/bin/goose-shell
Step 2: Restructure ~/.bashrc
Move all environment setup (PATH, exports, devbox shellenv, sdkman)
above the interactive guard. Move completions and prompt
integration below it. Add [[ $- != *i* ]] && return as the
separator. Use the shell-local __devbox_shellenv_done guard
(not exported) for the devbox shellenv block.
Step 3: Set GOOSE_SHELL in desktop entry
Check if a user-level .desktop override exists:
ls ~/.local/share/applications/Goose.desktop 2>/dev/null
If it exists, add GOOSE_SHELL=/home/<user>/.local/bin/goose-shell
to the Exec=env ... line. If not, create one from the system file:
cp /usr/share/applications/Goose.desktop ~/.local/share/applications/Goose.desktop
Then edit the Exec line to include GOOSE_SHELL.
Step 4: Set GOOSE_SHELL in environment.d
mkdir -p ~/.config/environment.d
cat > ~/.config/environment.d/60-goose-shell.conf << EOF
GOOSE_SHELL=/home/$(whoami)/.local/bin/goose-shell
EOF
Step 5: Apply and verify
# Reload systemd user environment
systemctl --user daemon-reload
# Update desktop database
update-desktop-database ~/.local/share/applications/ 2>/dev/null
# Restart Goose Desktop, then test:
# In Goose Desktop, run: which gh && gh --version
Verification Checklist
GOOSE_SHELLis set in the Goose process environment (cat /proc/$(pgrep -f '/usr/lib/Goose/Goose' | head -1)/environ | tr '\0' '\n' | grep GOOSE_SHELL)- Shell commands see the full PATH (devbox/nix, cargo, crc/oc, sdkman)
which ghfinds gh at the devbox nix pathwhich ocfinds ocdevbox global shellenvruns without errors- Environment variables are set (
CLOUD_ML_REGION,SDKMAN_DIR, etc.) - Interactive bash sessions still work normally (completions, prompt)
- No
evalerrors in shell output - No runaway
devbox global shellenvprocesses after Goose Desktop restart (ps aux | grep 'devbox global shellenv' | grep -v grep | wc -lshould be 0) refresh-globalalias is available insidedevbox shellsessions (rundevbox shellthentype refresh-global)
Gotchas
-
Goose’s hermit
nodewrapper breaks non-Goose Node.js workflows. Goose ships/usr/lib/Goose/resources/bin/containing wrapper scripts fornode,npx,uvx, andjbang. These wrappers exist so Goose’s MCP server extensions can find a Node.js runtime even when the user hasn’t installed one. Each wrapper sourcesnode-setup-common.sh, whichcds into~/.config/goose/mcp-hermit/and prepends~/.config/goose/mcp-hermit/bin/to PATH before running the real command. This CWD change breaks any npm/pnpm post-install script that uses relative paths (e.g.,node scripts/prebuild.jsresolves to~/.config/goose/mcp-hermit/scripts/prebuild.jsinstead of the package directory).Affected binaries:
node,npx,uvx,jbangonly. Commands likebash,python3,curl,gitare NOT wrapped.Why
goose-shellcan’t fix this: The Goose Electron process itself injects/usr/lib/Goose/resources/bin/onto PATH before thegoose-shellwrapper runs. The wrapper sources~/.bashrcto restore user tools, but Goose’s bin dir still wins because it’s earlier on PATH.Per-skill workaround pattern (recommended for any skill that runs Node.js commands through Goose’s shell tool):
# Use system node directly NODE="/usr/bin/node" # Strip hermit from PATH for pnpm/npm build scripts CLEAN_PATH=$(echo "$PATH" | tr ':' '\n' \ | grep -v 'goose.*hermit' \ | grep -v 'Goose.*bin' \ | tr '\n' ':') CLEAN_PATH="${CLEAN_PATH%:}" PATH="$CLEAN_PATH" $NODE some-commandDo NOT fix globally by reordering PATH to put
/usr/binbefore Goose’s bin dir — this could break MCP server extensions that depend on hermit’s isolated Node.js runtime (which may be a different version than the system Node.js).Skills using this pattern:
dsh-setup(pnpm builds for DSH).
Affected Files
| File | Purpose |
|---|---|
~/.local/bin/goose-shell |
Wrapper: sources ~/.bashrc for non-interactive shells |
~/.local/share/applications/Goose.desktop |
Desktop entry: passes GOOSE_SHELL to Goose process |
~/.config/environment.d/60-goose-shell.conf |
Systemd user env: sets GOOSE_SHELL as fallback |
~/.bashrc |
Restructured: env setup for all shells, interactive guard, shell-local devbox guard |
Platform Notes
- Tested on Fedora with GNOME/Wayland desktop
- Goose Desktop is an Electron app installed at
/usr/lib/Goose/ - The Goose
shelltool honors theGOOSE_SHELLenvironment variable to select the shell binary (defaults to/bin/bash) - The
environment.dmechanism works for apps launched via systemd user session (GNOME on Fedora). Other desktop environments may need different approaches. - If nix is not installed, skip the
unset __ETC_PROFILE_NIX_SOURCEDline in the wrapper
Changelog
See CHANGELOG.md for version history.