Skip to content

fix(install): preserve hooks and recover from installation failures - #11

Open
AnnatarHe wants to merge 5 commits into
mainfrom
codex/product-polish-2026-10-02
Open

AnnatarHe wants to merge 5 commits into
mainfrom
codex/product-polish-2026-10-02

Conversation

@AnnatarHe

@AnnatarHe AnnatarHe commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Failed hook downloads could replace working installations, paths with spaces broke shell startup, and upgrades could append a second source line that tracked every command twice.

  • Download into a private temporary directory with cleanup, HTTP failure handling, and timeouts.
  • Migrate legacy unquoted source lines and remove duplicate generated entries. Preserve shell-config permissions and handle spaced home directories.
  • Replace hooks/backups only after a successful download, set sourced hooks to 0644 and binaries to 0755, and continue remaining hooks and daemon setup after an individual hook failure before exiting nonzero.
  • Recognize Linux arm64/aarch64 and reject unsupported systems, including native MINGW/MSYS/Cygwin. Windows users can use WSL; the old native Windows branches were unreachable.
  • Make helper functions sourceable for isolated regression tests and run them in Linux/macOS CI, including the Bash installation path.

Validation: Bash and Zsh syntax checks and all 16 isolated Python tests passed. Tests cover failed archives/hooks, backup preservation, permissions, upgrade duplication, spaces, partial installs, successful reruns, platform mapping, piped execution, symlinked configs, missing trailing newlines, interruption cleanup, and preserved caller traps. Hook source pinning/checksums remain a separate follow-up.

Interruption follow-up: keep active hooks in place while copying backups into temporary files, then atomically replace both backups and hooks. Resolve shell-config symlinks and replace their targets atomically while preserving permissions. Clean temporary files on exit or interruption, and document Python 3 for tests.

CI follow-up: initialize Bash, Zsh, and Fish profile fixtures before installation. The six Linux/macOS shell matrix jobs passed after this fixture correction; the final backup-copy regression also passes locally.

@claude

claude Bot commented Oct 2, 2026

Copy link
Copy Markdown

Review

Good direction overall: temp-dir + trap cleanup, curl -f with timeouts, and download-before-replace are real improvements. A few issues, the first being the most important.

Bugs / risks

  1. Upgrades will source hooks twice. add_source_to_config now writes source "<path>" and greps for that exact quoted string. Existing users already have the unquoted source /home/me/.shelltime/hooks/zsh.zsh line, so it won't match and a second line gets appended. Every command is then tracked twice. Please match both forms (e.g. grep -F on the path alone) or migrate the old line.
  2. Partial installs. With set -e and return 1, if fish.fish fails to download the script aborts after zsh.zsh was already replaced, before any source lines are added or the daemon is reinstalled. Consider continuing past a failed hook, recording the failure, and exiting non-zero at the end.
  3. .bak is deleted before the download is known to succeed. check_and_delete_bak runs up front for zsh/fish, so a failed download still discards the previous backup. Move the cleanup into process_file after a successful download. bash.bash and bash-preexec.sh backups are never cleaned up (pre-existing).
  4. Hook file permissions. The PR description says permissions are set explicitly, but I don't see a chmod for hooks. mktemp creates files as 0600, so hooks change from the umask default to 0600. Probably fine for sourced files, but please set it explicitly.
  5. Progress meter noise. curl -sSLO became curl -fSLO; dropping -s prints a progress bar into curl | bash output. Use -fsSLO.
  6. Dead code. After the Darwin|Linux gate, the Windows branch in get_download_url and the MINGW/MSYS note are unreachable. Also, if [ $? -ne 0 ] after mkdir -p never runs under set -e (the script exits first), so those friendly messages won't show; use if ! mkdir -p ....
  7. set -u is new; worth a pass over variables in untaken branches to make sure nothing is read unset.

Tests

  • The PR says paths with spaces are tested, but I don't see such a test in tests/test_install.py. It should be covered (e.g. call add_source_to_config with a HOME containing a space, and run it twice to catch the duplication in item 1).
  • test_hook_failure_preserves_working_file slices process_file out of the script by string index; this breaks silently if the layout changes. Prefer a sourceable script, or at least assert the slice is non-empty.
  • Test 1 doesn't override HOME. It exits early today, but a future change could touch the real home. Point HOME at the temp dir.
  • No tests for arm64/aarch64 mapping or the unsupported-OS error; both are cheap with the uname stub.
  • The Python tests aren't wired into CI, so they won't run on PRs. Please add python3 -m unittest discover -s tests to the workflow.

Security

Nothing new introduced; mktemp -d is better than the predictable /tmp name. Hooks and bash-preexec are still fetched from master and sourced without checksum or pinning, which is worth a follow-up.

Minor

  • Per CLAUDE.md, use a scope in the title, e.g. fix(install): ....

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

Copy link
Copy Markdown

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: d42ea0d8dd

ℹ️ 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 install.bash Outdated
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T10:39:23.378371Z 3d36b45 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 2, 2026

Copy link
Copy Markdown

Review

The core change is sound. Hooks are downloaded to a temp file and only swapped in after the download succeeds. curl -f and timeouts are added. The add_source_to_config migration and the failure-path tests are good. Comments below, roughly by importance.

Issues

  1. set -euo pipefail on the unchanged middle of the script. The PATH/profile section (about lines 200–245) is outside the diff. Any grep -q that returns 1, any unset variable, or any pipeline that fails there will now abort the install silently. The tests don't cover every shell/profile combination, so please check that section. This is especially worth doing with -u on macOS's bash 3.2.
  2. Behaviour change when the daemon dir can't be created. mkdir -p ~/.shelltime/daemon failing still prints "Warning", but it now does return 1, which under set -e ends the whole install. Hooks are never installed. Either make it a real error message or keep it non-fatal. For the hooks dir, aborting is arguably right.
  3. PR description doesn't match the code. It says hooks get "explicitly set executable permissions", but process_file does chmod 644. 644 is fine for sourced files, so I'd update the description.
  4. Windows support removed without discussion. The MINGW/MSYS/CYGWIN branches were effectively dead, because uname -s returns e.g. MINGW64_NT-10.0, so they never matched. Rejecting them early is reasonable, but say so in the PR description. It's a user-visible policy change.
  5. Upgrade ordering in process_file. The old hook is moved to .bak and then the new one is moved in. A crash between the two mvs leaves no hook at all. Copying to .bak (cp -p) and then doing a single atomic mv over the target avoids that window.
  6. Leftover temp files. mktemp files in $hooks_path and next to the rc files aren't cleaned up if the script is interrupted (Ctrl-C, SIGTERM). Only $curr_time_dir has a trap.
  7. The trap ... EXIT leaks into the caller when the script is sourced. This only matters for the tests, which source the script.

Minor / pre-existing

  • Hooks and bash-preexec.sh are fetched from master branches with no checksum or pinning. CLAUDE.md names main as the main branch, so please confirm the shelltime/installation master URLs actually resolve.
  • The binary install (mv shelltime ~/.shelltime/bin/) is still not atomic or rollback-safe, which is inconsistent with the goal for hooks.
  • add_source_to_config rewrites via cat > file. That preserves symlinks and permissions, but it is not atomic, so an interruption mid-write could truncate the user's rc file.

Tests

  • Coverage is good: failure paths, idempotency, paths with spaces, arm64, piped execution, and unsupported OS.
  • Add a case for an rc file with no trailing newline, and one for an rc file that is a symlink, since the code comments claim symlinks are preserved.
  • The new bash CI step does a real network install, so it can be flaky. That is pre-existing for zsh and fish.
  • This repo is otherwise shell-only. Mention the Python test requirement in CLAUDE.md's Testing section.

Style

  • Per CLAUDE.md, the title should be a Conventional Commit with scope, e.g. fix(install): ....
  • The body of install_shelltime is left unindented to keep the diff small. A follow-up reindent would help readability.

@AnnatarHe AnnatarHe changed the title fix: make installation failures recoverable and preserve existing hooks fix(install): preserve hooks and recover from installation failures Oct 2, 2026
@claude

claude Bot commented Oct 2, 2026

Copy link
Copy Markdown

Review

Overall this is a solid robustness improvement: temp-dir downloads, atomic replace via mktemp + mv, symlink-aware config edits, legacy-source migration, aarch64 support, and sourceable functions for tests. I read install.bash only. I did not run it or read tests/test_install.py.

Potential issues

  1. add_source_to_config failures abort the whole run. The PR says the installer continues after an individual failure. But under set -e, a failed call (for example an unwritable rc file or a mktemp failure) exits immediately. That skips the other shells, the daemon reinstall, and the "incomplete" message. Wrap the calls like process_file and set installation_failed=true:
    add_source_to_config ... || installation_failed=true
  2. .bak is overwritten on every run. cp -p file file.bak replaces the previous backup. A rerun after a bad-but-HTTP-200 download, or after a hook someone edited by hand, destroys the last known-good copy. Consider skipping the copy when the new content is identical (cmp -s), or keep the backup only on the first change.
  3. Config file ownership and ACLs. mktemp + mv creates a new inode. cp -p keeps the mode, but ownership and xattrs are not guaranteed. This matters if .zshrc is owned by another user or the installer runs under sudo. This is an edge case, but it is worth a comment.
  4. PATH edits are not atomic or deduplicated by the same logic. The .zshrc, .bashrc, and config.fish PATH blocks still use echo >> and a grep '$HOME/.shelltime/bin' check. That is inconsistent with the new careful handling, but it is not a regression.
  5. Manual-install section is unchanged and weaker than the hook path. mv shelltime "$HOME/.shelltime/bin/" is not atomic and does not roll back if shelltime-daemon fails afterwards. unzip has no -o, but it runs in a fresh temp dir, so that is fine. The if [[ "$OS" == ... ]] guards around the move and PATH blocks are now always true and could be removed. Likewise, the get_download_url branch for unsupported OS is unreachable after the early case.
  6. Indentation. The body of if [ "$BREW_INSTALLED" = false ]; then is not indented, which hurts readability. Consider extracting it into a function such as install_binary.
  7. trap ... EXIT inside install_shelltime. This overrides any caller EXIT trap if the script is sourced. It is fine for tests, but worth noting in a comment.

Security

  • Hooks come from the master branch of raw.githubusercontent, and bash-preexec.sh comes from a third-party master. The CLI release archive has no checksum or signature check either. The PR acknowledges this as follow-up work. I'd prioritise pinning to a tag or commit and verifying checksums, because these files are sourced into every interactive shell.
  • Temp files are created with mktemp, and the paths are quoted. Both are good.

Performance

  • Nothing significant. The timeouts (15s connect, 60s or 300s max) are sensible.

Tests and CI

  • The 11 isolated tests cover the main failure modes, and adding them to the Linux and macOS CI is good. I'd add cases for a failing add_source_to_config (see item 1), for config files that are symlinks to a different directory, and for relative symlink chains. readlink without -f is handled by the loop, which is good.
  • The CI shell matrix still only checks that the install succeeds. A step that sources each hook in its shell and checks that shelltime track is called exactly once would catch double-tracking regressions end to end.

Minor

  • CLAUDE.md and README.md changes look fine. The commit scope fix(install) matches the repo conventions.

Nice work. Item 1 is the one I'd fix before merging.

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

Copy link
Copy Markdown

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: 92081bd422

ℹ️ 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 install.bash Outdated
@claude

claude Bot commented Oct 2, 2026

Copy link
Copy Markdown

Review

Solid hardening pass. Downloads go to a temp file and replace the old one only on success, curl has -f and timeouts, config edits are atomic and symlink-aware, and the legacy source line is migrated and deduplicated. I read install.bash only and did not run the tests.

Potential bugs / robustness

  1. add_source_to_config failures skip the summary. The calls aren't wrapped in if !, so under set -e a failure aborts immediately. The user gets no "Installation incomplete" message and the daemon reinstall is skipped. Set installation_failed=true as process_file does.
  2. The trap calls overwrite caller traps. The PR says caller traps are preserved, but install_shelltime replaces any existing EXIT/INT/TERM trap and leaves its own installed when sourced. Chain the previous trap, or only install traps when run directly. Please check the test actually exercises this.
  3. The migration only matches exact lines. source /path # comment, . /path and source '/path' aren't recognised, so those users can still end up double-sourced after upgrading.
  4. The .bak is overwritten on every run. A bad hook installed once replaces the last good backup on the next rerun. Consider skipping the backup when content is identical.
  5. The PATH-export blocks still use the old style. Unquoted grep pattern and plain >> appends, with no symlink or trailing-newline handling. Inconsistent with the new hardening.

Security

  • Hooks are still fetched from the mutable master branch and the CLI archive has no checksum check. The PR calls this a follow-up, which is fair. Please file an issue so it isn't lost.
  • Temp files created next to the target with mktemp keep the mv atomic. Good.

Style

  • The repeated if ! cmd; then rm -f ...; return 1; fi blocks could be trimmed, since the EXIT cleanup already removes tracked files.
  • The manual-install block is large and unindented inside if [ "$BREW_INSTALLED" = false ]. Extracting it into a function would help readability and testability.
  • Commit messages should follow the CLAUDE.md Conventional Commits with scope format.

Tests
Coverage is good. I'd add cases for the add_source_to_config failure path (item 1) and for commented or .-style legacy source lines (item 3).

None of these are blockers except possibly item 1.

@claude

claude Bot commented Oct 2, 2026

Copy link
Copy Markdown

Review

Solid hardening pass. Temp-file downloads, atomic replacement, quoted source lines and upgrade de-duplication fix real bugs, and the stubbed uname/curl tests suit a shell script well. I read the diff and did not run the tests.

Potential bugs / behavior

  1. set -euo pipefail conflicts with "continue after failure". add_source_to_config is called bare. If it returns 1 (e.g. ~/.zshrc symlinked into a read-only store like Nix/home-manager so mktemp fails, or a dangling symlink), errexit aborts the script. Daemon reinstall is skipped and no "Installation incomplete" message is printed. Treat it like hooks: if ! add_source_to_config ...; then installation_failed=true; fi.
  2. Binary install is still not atomic. mv shelltime "$HOME/.shelltime/bin/" crosses filesystems when /tmp is tmpfs, so mv copies straight onto the final path. An interrupt leaves a truncated binary, which is the failure mode this PR fixes for hooks. Stage in ~/.shelltime/bin via mktemp, then mv.
  3. Source line quoting is unescaped. source "$path" handles spaces, but a HOME containing ", $, backtick or \ still breaks shell startup. Rare; worth a note or escaping.
  4. No loop guard in symlink resolution. A symlink cycle hangs the installer. Add a depth limit.
  5. Hooks are only validated by HTTP status. An empty 200 response replaces a working hook and its backup. A [ -s "$pending_file" ] check is cheap. Pinning/checksums as a follow-up is agreed, especially since bash-preexec is fetched from master and sourced in every shell.

Compatibility / design

  • Sourcing the script leaks set -euo pipefail into the caller's shell. Fine for the tests, but sourcing interactively could kill the shell. Consider setting it inside install_shelltime.
  • Windows removal is a behavior change for Git Bash/MSYS users (README mentions WSL, good). The .zip/unzip branch is now dead code and can be removed.
  • The BASH_SOURCE piped-execution check is subtle; a one-line comment would help.
  • The ~150-line flat-indented if [ "$BREW_INSTALLED" = false ] block would read and test better as an install_binary() function.

Tests / CI

  • Hook/config coverage is very good. Missing: Homebrew path (stub brew on Darwin), failed tar extraction or missing shelltime in the archive, dangling symlinked config.
  • test_interrupted_hook_download_cleans_pending_files patches the stub via string replace and would silently stop testing if the stub text changes. Assert the replacement happened.
  • The CI "Verify installation" step could also assert exactly one quoted source "..." line on real runners.
  • timeout=10 may be tight on slow macOS runners; consider 30.

Items 1 and 2 are the ones I'd fix before merging; the rest are suggestions.

🤖 Generated with Claude Code

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

Copy link
Copy Markdown

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: 3d36b451cf

ℹ️ 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 install.bash
Comment on lines +111 to +115
sub(/^[[:space:]]+/, "", line)
sub(/[[:space:]]+$/, "", line)
if (line == legacy || line == quoted) {
if (!found) print quoted
found = 1

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 Restrict deduplication to installer-owned source lines

When a user intentionally sources the hook in multiple mutually exclusive blocks, trimming indentation makes every occurrence look like a previously generated top-level line, and the global found flag deletes all but the first. For example, two indented source commands in an if/else are reduced to one, leaving the else branch empty and making .bashrc syntactically invalid on the next shell launch. Only deduplicate the exact top-level spellings emitted by prior installer versions, rather than normalized lines inside user-controlled blocks.

Useful? React with 👍 / 👎.

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