Skip to content

feat(cli): support Homebrew binary paths for daemon and hooks#272

Merged
AnnatarHe merged 2 commits intomainfrom
feat/homebrew-path-support
Apr 12, 2026
Merged

feat(cli): support Homebrew binary paths for daemon and hooks#272
AnnatarHe merged 2 commits intomainfrom
feat/homebrew-path-support

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

@AnnatarHe AnnatarHe commented Apr 12, 2026

Summary

  • Add ResolveDaemonBinaryPath() that searches ~/.shelltime/bin, PATH, and standard Homebrew locations (/opt/homebrew/bin, /usr/local/bin, /home/linuxbrew/.linuxbrew/bin)
  • Update macOS plist and Linux systemd templates to use the resolved {{.DaemonBinPath}} instead of hardcoded {{.BaseFolder}}/bin/shelltime-daemon
  • Embed shell hook scripts (bash.bash, zsh.zsh, fish.fish) via go:embed so shelltime hooks install works without the curl installer
  • Auto-download bash-preexec.sh dependency during bash hooks install
  • Update shelltime hooks install to accept shelltime on PATH (Homebrew) in addition to ~/.shelltime/bin
  • Add Homebrew as primary install method in README

Test plan

  • go build ./... passes
  • go vet ./model/... ./commands/... ./daemon/... passes
  • go test -timeout 3m ./... passes
  • Verify shelltime daemon install resolves binary correctly for Homebrew installs
  • Verify shelltime hooks install creates hook files in ~/.shelltime/hooks/ for Homebrew users
  • Verify existing curl-installer users are unaffected (backward compatible)

🤖 Generated with Claude Code


Open with Devin

Homebrew installs binaries to /opt/homebrew/bin (Apple Silicon) or
/usr/local/bin (Intel), not ~/.shelltime/bin. This adds proper binary
resolution across install methods and embeds hook scripts so Homebrew
users don't need the curl installer for shell hooks.

- Add ResolveDaemonBinaryPath() to search curl-installer, PATH, and
  Homebrew fallback locations
- Update plist/systemd templates to use resolved DaemonBinPath
- Embed bash/zsh/fish hook scripts via go:embed for self-contained
  Homebrew installs
- Auto-download bash-preexec.sh dependency during hooks install
- Update hooks install to accept shelltime on PATH (not just in
  ~/.shelltime/bin)
- Add Homebrew install method to README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Homebrew support for shelltime by updating the installation logic to resolve the daemon binary from multiple locations (Homebrew, curl-installer, or PATH) and embedding shell hook scripts directly into the binary. Feedback includes suggestions to background shell tracking and garbage collection tasks in hook scripts to reduce shell latency, adding timeouts and size limits to network requests when downloading dependencies, and improving error handling during daemon installation. Additionally, a more efficient method for retrieving the parent process ID in Fish shell was recommended.

Comment thread model/hooks/bash.bash
return
fi

shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=pre --ppid=$PPID &> /dev/null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Executing shelltime track synchronously before every command adds latency to the shell. Backgrounding the process prevents blocking the user's workflow.

Suggested change
shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=pre --ppid=$PPID &> /dev/null
shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=pre --ppid=$PPID &> /dev/null &

Comment thread model/hooks/bash.bash
return
fi

shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The post-command tracking should also be backgrounded to ensure the prompt returns immediately after a command finishes.

Suggested change
shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null
shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null &

Comment thread model/hooks/fish.fish
return
end
# This event is triggered before each prompt, which is after each command
shelltime track -s=fish -id=$SESSION_ID -cmd="$argv" -p=post -r=$LAST_RESULT --ppid=$FISH_PPID > /dev/null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Backgrounding the tracking call in Fish prevents latency between command completion and the next prompt. Also, redirecting stderr ensures that any errors from the tracking command don't clutter the terminal.

    shelltime track -s=fish -id=$SESSION_ID -cmd="$argv" -p=post -r=$LAST_RESULT --ppid=$FISH_PPID > /dev/null 2>&1 &

Comment thread model/hooks/zsh.zsh
if [[ $CMD =~ ^(exit|logout|reboot) ]]; then
return
fi
shelltime track -s=zsh -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Backgrounding the tracking call in Zsh ensures that the prompt returns immediately after a command finishes, avoiding any perceived lag.

    shelltime track -s=zsh -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null &

color.Yellow.Println("⚠️ shelltime-daemon not found.")
color.Yellow.Println("Install via Homebrew: brew install shelltime/tap/shelltime")
color.Yellow.Println("Or via curl installer: curl -sSL https://shelltime.xyz/i | bash")
return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The command returns nil when the daemon binary is not found. It should return an error to ensure the CLI exits with a non-zero status code, which is essential for scripts and automation to detect failures.

Suggested change
return nil
return fmt.Errorf("shelltime-daemon binary not found")

Comment thread model/shell.bash.go Outdated
return nil // already exists
}

resp, err := http.Get(bashPreexecURL)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Using http.Get without a timeout can cause the process to hang indefinitely if the network is unstable. It's safer to use an http.Client with a reasonable timeout.

	client := &http.Client{Timeout: 15 * 1000 * 1000 * 1000} // 15 seconds
	resp, err := client.Get(bashPreexecURL)

Comment thread model/shell.bash.go Outdated
return fmt.Errorf("failed to download bash-preexec.sh: HTTP %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Reading the entire response body without a size limit can lead to high memory consumption if the server returns an unexpectedly large response. Using io.LimitReader is a more secure practice.

Suggested change
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))

Comment thread model/hooks/bash.bash
then
echo "Warning: shelltime CLI not found. Please install it to enable time tracking."
else
shelltime gc
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Running shelltime gc synchronously on every shell startup can slow down terminal opening. Backgrounding this task improves shell responsiveness.

Suggested change
shelltime gc
shelltime gc &> /dev/null &

Comment thread model/hooks/fish.fish
set -g SESSION_ID (date +%Y%m%d%H%M%S)

# Capture parent process ID at shell startup (fish doesn't have native $PPID)
set -g FISH_PPID (ps -o ppid= -p %self | string trim)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Fish shell, status ppid is a more efficient and built-in way to retrieve the parent process ID compared to spawning an external ps process.

set -g FISH_PPID (status ppid)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 12, 2026

Codecov Report

❌ Patch coverage is 0% with 89 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
model/shell.bash.go 0.00% 21 Missing ⚠️
model/path.go 0.00% 15 Missing ⚠️
commands/daemon.install.go 0.00% 13 Missing ⚠️
model/shell.go 0.00% 9 Missing ⚠️
model/daemon-installer.darwin.go 0.00% 8 Missing ⚠️
commands/hooks.install.go 0.00% 6 Missing ⚠️
model/daemon-installer.linux.go 0.00% 5 Missing ⚠️
model/daemon-installer.go 0.00% 3 Missing ⚠️
model/shell.fish.go 0.00% 3 Missing ⚠️
model/shell.zsh.go 0.00% 3 Missing ⚠️
... and 3 more
Flag Coverage Δ
unittests 39.56% <0.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
commands/daemon.status.go 0.00% <0.00%> (ø)
commands/daemon.uninstall.go 0.00% <0.00%> (ø)
commands/doctor.go 0.00% <0.00%> (ø)
model/daemon-installer.go 0.00% <0.00%> (ø)
model/shell.fish.go 47.72% <0.00%> (-3.50%) ⬇️
model/shell.zsh.go 51.16% <0.00%> (-3.84%) ⬇️
model/daemon-installer.linux.go 0.00% <0.00%> (ø)
commands/hooks.install.go 0.00% <0.00%> (ø)
model/daemon-installer.darwin.go 0.00% <0.00%> (ø)
model/shell.go 66.66% <0.00%> (-12.50%) ⬇️
... and 3 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02368fbe0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread model/hooks/bash.bash
Comment on lines +4 to +5
if [ -f "bash-preexec.sh" ]; then
source "bash-preexec.sh"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid sourcing bash-preexec from current directory

The new Bash hook will source bash-preexec.sh from the caller’s current working directory before checking the ShellTime hook directory, which allows arbitrary local files to be executed whenever a user starts a Bash subshell in an untrusted folder. This is a security regression for any user who installs embedded hooks, because a project containing a malicious bash-preexec.sh would be executed implicitly.

Useful? React with 👍 / 👎.

Comment thread model/shell.bash.go Outdated
return nil // already exists
}

resp, err := http.Get(bashPreexecURL)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Set a timeout for bash-preexec download requests

ensureBashPreexec uses http.Get with the default client, which has no request timeout, so shelltime hooks install can hang indefinitely on slow/broken networks (for example, captive portals or stalled TLS handshakes). Adding a bounded timeout avoids a user-visible freeze during installation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a timeout, 1 mins @claude

Copy link
Copy Markdown
Contributor

@claude claude Bot Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @AnnatarHe's task in 1m 14s —— View job


Adding HTTP timeout to bash-preexec download

  • Read current model/shell.bash.go
  • Add 1-minute timeout to http.Get call
  • Commit and push changes

Added a 1-minute timeout via http.Client{Timeout: 1 * time.Minute} to replace the default http.Get (which has no timeout). Also added io.LimitReader(resp.Body, 1024*1024) to cap the response body at 1MB as an additional safety measure.
|

…download

Use http.Client with 1-minute timeout instead of http.Get to prevent
hanging on slow/broken networks. Also add io.LimitReader (1MB) to
prevent excessive memory usage from unexpected large responses.

Co-authored-by: Le He <AnnatarHe@users.noreply.github.com>
@AnnatarHe AnnatarHe merged commit 8e0b3ea into main Apr 12, 2026
1 of 3 checks passed
@AnnatarHe AnnatarHe deleted the feat/homebrew-path-support branch April 12, 2026 17:54
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