fix: inherit environment in daemon subprocess calls (#876)

The daemon's exec.Command calls were not explicitly setting cmd.Env,
causing subprocesses to fail when the daemon process doesn't have
the expected PATH environment variable. This manifests as:

  Warning: failed to fetch deacon inbox: exec: "gt": executable file not found in $PATH

When the daemon is started by mechanisms with minimal environments
(launchd, systemd, or shells without full PATH), executables like
gt, bd, git, and sqlite3 couldn't be found.

The fix adds cmd.Env = os.Environ() to all 15 subprocess calls across
three files, ensuring they inherit the daemon's full environment.

Affected commands:
- gt mail inbox/delete/send (lifecycle requests, notifications)
- bd sync/show/list/activity (beads operations)
- git fetch/pull (workspace pre-sync)
- sqlite3 (convoy completion queries)

Fixes #875

Co-authored-by: Jackson Cantrell <cantrelljax@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jackson Cantrell
2026-01-22 16:41:51 -08:00
committed by GitHub
parent 16d3a92455
commit 0fb3e8d5fe
3 changed files with 16 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
@@ -87,6 +88,7 @@ func (w *ConvoyWatcher) run() {
func (w *ConvoyWatcher) watchActivity() error {
cmd := exec.CommandContext(w.ctx, "bd", "activity", "--follow", "--town", "--json")
cmd.Dir = w.townRoot
cmd.Env = os.Environ() // Inherit PATH to find bd executable
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -168,6 +170,7 @@ func (w *ConvoyWatcher) getTrackingConvoys(issueID string) []string {
`, safeIssueID, safeIssueID)
queryCmd := exec.Command("sqlite3", "-json", dbPath, query)
queryCmd.Env = os.Environ() // Inherit PATH to find sqlite3 executable
var stdout bytes.Buffer
queryCmd.Stdout = &stdout
@@ -200,6 +203,7 @@ func (w *ConvoyWatcher) checkConvoyCompletion(convoyID string) {
strings.ReplaceAll(convoyID, "'", "''"))
queryCmd := exec.Command("sqlite3", "-json", dbPath, convoyQuery)
queryCmd.Env = os.Environ() // Inherit PATH to find sqlite3 executable
var stdout bytes.Buffer
queryCmd.Stdout = &stdout
@@ -224,6 +228,7 @@ func (w *ConvoyWatcher) checkConvoyCompletion(convoyID string) {
checkCmd := exec.Command("gt", "convoy", "check", convoyID)
checkCmd.Dir = w.townRoot
checkCmd.Env = os.Environ() // Inherit PATH to find gt executable
var checkStdout, checkStderr bytes.Buffer
checkCmd.Stdout = &checkStdout
checkCmd.Stderr = &checkStderr

View File

@@ -1101,6 +1101,7 @@ Manual intervention may be required.`,
cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body) //nolint:gosec // G204: args are constructed internally
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find gt executable
if err := cmd.Run(); err != nil {
d.logger.Printf("Warning: failed to notify witness of crashed polecat: %v", err)
}

View File

@@ -40,6 +40,7 @@ func (d *Daemon) ProcessLifecycleRequests() {
// Get mail for deacon identity (using gt mail, not bd mail)
cmd := exec.Command("gt", "mail", "inbox", "--identity", "deacon/", "--json")
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find gt executable
output, err := cmd.Output()
if err != nil {
@@ -576,6 +577,7 @@ func (d *Daemon) syncWorkspace(workDir string) {
fetchCmd := exec.Command("git", "fetch", "origin")
fetchCmd.Dir = workDir
fetchCmd.Stderr = &stderr
fetchCmd.Env = os.Environ() // Inherit PATH to find git executable
if err := fetchCmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
@@ -592,6 +594,7 @@ func (d *Daemon) syncWorkspace(workDir string) {
pullCmd := exec.Command("git", "pull", "--rebase", "origin", defaultBranch)
pullCmd.Dir = workDir
pullCmd.Stderr = &stderr
pullCmd.Env = os.Environ() // Inherit PATH to find git executable
if err := pullCmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
@@ -608,6 +611,7 @@ func (d *Daemon) syncWorkspace(workDir string) {
bdCmd := exec.Command("bd", "sync")
bdCmd.Dir = workDir
bdCmd.Stderr = &stderr
bdCmd.Env = os.Environ() // Inherit PATH to find bd executable
if err := bdCmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
@@ -625,6 +629,7 @@ func (d *Daemon) closeMessage(id string) error {
// Use gt mail delete to actually remove the message
cmd := exec.Command("gt", "mail", "delete", id)
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find gt executable
output, err := cmd.CombinedOutput()
if err != nil {
@@ -662,6 +667,7 @@ func (d *Daemon) getAgentBeadState(agentBeadID string) (string, error) {
func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
cmd := exec.Command("bd", "show", agentBeadID, "--json")
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find bd executable
output, err := cmd.Output()
if err != nil {
@@ -799,6 +805,7 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
// Pattern: <prefix>-<rig>-polecat-<name> (e.g., gt-gastown-polecat-Toast)
cmd := exec.Command("bd", "list", "--type=agent", "--json")
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find bd executable
output, err := cmd.Output()
if err != nil {
@@ -874,6 +881,7 @@ Action needed: Check if agent is alive and responsive. Consider restarting if st
cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body)
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find gt executable
if err := cmd.Run(); err != nil {
d.logger.Printf("Warning: failed to notify witness of GUPP violation: %v", err)
@@ -897,6 +905,7 @@ func (d *Daemon) checkOrphanedWork() {
func (d *Daemon) checkRigOrphanedWork(rigName string) {
cmd := exec.Command("bd", "list", "--type=agent", "--json")
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find bd executable
output, err := cmd.Output()
if err != nil {
@@ -970,6 +979,7 @@ Action needed: Either restart the agent or reassign the work.`,
cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body)
cmd.Dir = d.config.TownRoot
cmd.Env = os.Environ() // Inherit PATH to find gt executable
if err := cmd.Run(); err != nil {
d.logger.Printf("Warning: failed to notify witness of orphaned work: %v", err)