From f577dc2a52dc824868f7b0ae2793a990d29ba21b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 11:12:18 +0000 Subject: [PATCH 1/2] feat(install): add cloud.bash for Claude Code cloud environments A cloud environment's setup script can now set ShellTime up with one line: curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash instead of the long block the guide had people paste. The script does the same thing: installs the Linux CLI and daemon into ~/.shelltime/bin, runs `shelltime cc install`, and registers a SessionStart hook that signs in with SHELLTIME_TOKEN and starts the daemon in every session. Fixes now reach an environment whenever it rebuilds its snapshot. A setup script that exits non-zero keeps sessions from starting, so an EXIT trap reports any failure and exits 0, and nothing runs before the final main call, so a truncated download installs nothing. raw.githubusercontent.com is on the cloud environments' default allowlist, so no new domain is needed. CI runs it twice on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, a single SessionStart hook and a silent hook without a token. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CXwVbjYvMsYbyyrcZnyJYV --- .github/workflows/test.yaml | 36 +++++++++++++ CLAUDE.md | 3 ++ README.md | 10 ++++ cloud.bash | 103 ++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100755 cloud.bash diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e690409..a4dbf1f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -66,3 +66,39 @@ jobs: ls ~/.shelltime/bin fi ls ~/.shelltime/hooks + + test-cloud: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # x86_64 and arm64, the two Linux builds cloud.bash downloads + os: ["ubuntu-latest", "ubuntu-24.04-arm"] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Run cloud.bash the way a setup script does, twice + run: | + set -euo pipefail + for run in 1 2; do + out=$(cat ./cloud.bash | bash 2>&1) || { echo "$out"; echo "cloud.bash exited non-zero"; exit 1; } + echo "$out" + echo "$out" | grep -q "ShellTime is installed" || { echo "run $run did not finish"; exit 1; } + done + + - name: Verify installation + run: | + set -euo pipefail + hook="$HOME/.shelltime/bin/cloud-session-start.sh" + settings="$HOME/.claude/settings.json" + "$HOME/.shelltime/bin/shelltime" --version + [ -x "$HOME/.shelltime/bin/shelltime-daemon" ] || { echo "shelltime-daemon missing"; exit 1; } + [ -x "$hook" ] || { echo "SessionStart hook missing"; exit 1; } + jq -e '.env.OTEL_EXPORTER_OTLP_ENDPOINT' "$settings" + count=$(jq --arg cmd "$hook" '[.hooks.SessionStart[].hooks[] | select(.command == $cmd)] | length' "$settings") + [ "$count" = 1 ] || { echo "expected one SessionStart hook, found $count"; exit 1; } + # Without SHELLTIME_TOKEN the hook does nothing and prints nothing + out=$(env -u SHELLTIME_TOKEN "$hook") + [ -z "$out" ] || { echo "hook printed: $out"; exit 1; } diff --git a/CLAUDE.md b/CLAUDE.md index 90c88f0..d917232 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ This is the **shelltime installation** repository - a collection of shell script ## Repository Structure - `install.bash` - Main installation script that downloads shelltime CLI binaries and sets up shell hooks +- `cloud.bash` - Setup for Claude Code cloud environments, run from an environment's setup script with `curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash`. Installs the Linux CLI and daemon into `~/.shelltime/bin`, runs `shelltime cc install`, and adds a Claude Code SessionStart hook (`~/.shelltime/bin/cloud-session-start.sh`, embedded in the script) that signs in with `SHELLTIME_TOKEN` and starts the daemon in each session. A setup script that exits non-zero stops sessions from starting, so it always exits 0 (its `EXIT` trap reports a failure instead), and nothing runs before the final `main` call, so a truncated download installs nothing. The guide that documents it is `content/blogs/track-claude-code-cloud-sessions/` in the web repo; keep the two in step - `hooks/` - Shell-specific hook scripts that track command execution: - `zsh.zsh` - Uses zsh's native `preexec` and `precmd` hooks - `bash.bash` - Uses `bash-preexec` library for preexec/precmd functionality @@ -28,6 +29,8 @@ The CI verifies: 2. Shell configuration files are properly sourced 3. Hook files are placed in `~/.shelltime/hooks/` +The `test-cloud` job runs `cloud.bash` twice on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, that the SessionStart hook is registered exactly once, and that the hook prints nothing without `SHELLTIME_TOKEN`. + ## How Shell Hooks Work Each shell hook script: diff --git a/README.md b/README.md index 3c1f8eb..f8d0cea 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,16 @@ You can install shelltime tools by running the following command in your termina curl -sSL https://shelltime.xyz/i | bash ``` +## Claude Code Cloud Environments + +To track Claude Code sessions that run in the cloud ([claude.ai/code](https://claude.ai/code)), add this line to the cloud environment's setup script: + +```bash +curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash +``` + +The environment also needs your ShellTime API token as `SHELLTIME_TOKEN` in its environment variables, and `api.shelltime.xyz` allowed by its network access. [cloud.bash](cloud.bash) installs the CLI and daemon, connects Claude Code, and adds a SessionStart hook that signs in with the token and starts the daemon in every session. It never fails the setup script. The full guide: [Track your Claude Code cloud sessions](https://shelltime.xyz/blogs/track-claude-code-cloud-sessions). + ## What Next? Once the installation script finishes successfully, you should be able to track your shell time. diff --git a/cloud.bash b/cloud.bash new file mode 100755 index 0000000..997fcec --- /dev/null +++ b/cloud.bash @@ -0,0 +1,103 @@ +#!/bin/bash +# +# Sets ShellTime up in a Claude Code cloud environment (claude.ai/code), so the +# sessions started in it are tracked like the ones on your own machines. Add +# this line to the environment's setup script: +# +# curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash +# +# The environment also needs SHELLTIME_TOKEN in its environment variables and +# api.shelltime.xyz allowed by its network access. Setup scripts don't see the +# environment's variables, so this only installs; a SessionStart hook signs in +# with the token in every session. +# +# Guide: https://shelltime.xyz/blogs/track-claude-code-cloud-sessions + +ST_DIR="$HOME/.shelltime" +HOOK_PATH="$ST_DIR/bin/cloud-session-start.sh" +SETTINGS_PATH="$HOME/.claude/settings.json" +RELEASE_URL="https://github.com/shelltime/cli/releases/latest/download" + +# A setup script that exits non-zero keeps the environment's sessions from +# starting, so a failure is reported here and never passed on. +finish() { + local status=$? + set +e + [ -n "${TMP_DIR:-}" ] && rm -rf "$TMP_DIR" + if [ "$status" -ne 0 ]; then + echo "ShellTime setup failed; sessions will not be tracked" >&2 + fi + exit 0 +} + +trap finish EXIT +set -euo pipefail + +# 1. CLI and daemon, where install.bash puts them (the ShellTime mod looks there) +install_cli() { + local arch + case "$(uname -m)" in + x86_64) arch=x86_64 ;; + aarch64 | arm64) arch=arm64 ;; + *) + echo "Unsupported architecture: $(uname -m)" >&2 + return 1 + ;; + esac + + mkdir -p "$ST_DIR/bin" "$ST_DIR/logs" "$HOME/.local/bin" + curl -fsSL --retry 3 -o "$TMP_DIR/cli.tar.gz" "$RELEASE_URL/cli_Linux_${arch}.tar.gz" + tar -xzf "$TMP_DIR/cli.tar.gz" -C "$TMP_DIR" shelltime shelltime-daemon + install -m 0755 "$TMP_DIR/shelltime" "$TMP_DIR/shelltime-daemon" "$ST_DIR/bin/" + ln -sf "$ST_DIR/bin/shelltime" "$HOME/.local/bin/shelltime" +} + +# 2. Claude Code's OTEL exporter pointed at the daemon +connect_claude_code() { + "$ST_DIR/bin/shelltime" cc install +} + +# 3. The snapshot keeps files but no processes, there is no systemd, and only +# the session sees SHELLTIME_TOKEN, so a SessionStart hook signs in and starts +# the daemon in every session +install_session_hook() { + cat >"$HOOK_PATH" <<'EOF' +#!/bin/bash +# Claude Code SessionStart hook, installed by ShellTime's cloud.bash. Its stdout +# would go into Claude's context, so it prints nothing. +st="$HOME/.shelltime" +[ -n "${SHELLTIME_TOKEN:-}" ] || exit 0 +# The first session, or a token changed in the environment settings +if ! grep -qF "$SHELLTIME_TOKEN" "$st/config.yaml" 2>/dev/null; then + "$st/bin/shelltime" auth --token "$SHELLTIME_TOKEN" >/dev/null 2>&1 || exit 0 +fi +if ! pgrep -fx "$st/bin/shelltime-daemon" >/dev/null; then + setsid nohup "$st/bin/shelltime-daemon" >>"$st/logs/shelltime-daemon.log" 2>&1 "$SETTINGS_PATH.tmp" + mv "$SETTINGS_PATH.tmp" "$SETTINGS_PATH" +} + +main() { + if [ "$(uname -s)" != "Linux" ]; then + echo "cloud.bash is for Claude Code cloud environments; on your own machine run: curl -sSL https://shelltime.xyz/i | bash" >&2 + return 1 + fi + + TMP_DIR=$(mktemp -d) + install_cli + connect_claude_code + install_session_hook + echo "ShellTime is installed; Claude Code sessions will be tracked once SHELLTIME_TOKEN is set" +} + +# Nothing runs before this line, so a download cut short installs nothing +main "$@" From a0408a4a92e4b6214b5870ddabf53af5ac9094d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 12:54:16 +0000 Subject: [PATCH 2/2] fix(install): run cloud.bash only where CLAUDE_CODE_REMOTE is true Cloud environments set CLAUDE_CODE_REMOTE=true, and it is never true on a user's own machine (https://code.claude.com/docs/en/cloud-environments). That is the check that matters: run by mistake on a laptop, cloud.bash would swap in Linux binaries and add a SessionStart hook, and a Linux desktop passed the old uname check. The setup script inherits the variable from environment-manager, which runs it. CI now checks that nothing is installed without the variable, and sets it for the real runs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CXwVbjYvMsYbyyrcZnyJYV --- .github/workflows/test.yaml | 11 ++++++++++- CLAUDE.md | 4 ++-- README.md | 2 +- cloud.bash | 9 ++++++--- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a4dbf1f..93ffe00 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -79,11 +79,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Outside a cloud environment cloud.bash installs nothing + run: | + set -euo pipefail + out=$(cat ./cloud.bash | env -u CLAUDE_CODE_REMOTE bash 2>&1) || { echo "$out"; echo "cloud.bash exited non-zero"; exit 1; } + echo "$out" + echo "$out" | grep -q "only runs in Claude Code cloud environments" || { echo "missing the refusal"; exit 1; } + [ ! -e "$HOME/.shelltime" ] || { echo "~/.shelltime was created"; exit 1; } + - name: Run cloud.bash the way a setup script does, twice run: | set -euo pipefail for run in 1 2; do - out=$(cat ./cloud.bash | bash 2>&1) || { echo "$out"; echo "cloud.bash exited non-zero"; exit 1; } + # A cloud environment's setup script runs with CLAUDE_CODE_REMOTE=true + out=$(cat ./cloud.bash | CLAUDE_CODE_REMOTE=true bash 2>&1) || { echo "$out"; echo "cloud.bash exited non-zero"; exit 1; } echo "$out" echo "$out" | grep -q "ShellTime is installed" || { echo "run $run did not finish"; exit 1; } done diff --git a/CLAUDE.md b/CLAUDE.md index d917232..ddc1f68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ This is the **shelltime installation** repository - a collection of shell script ## Repository Structure - `install.bash` - Main installation script that downloads shelltime CLI binaries and sets up shell hooks -- `cloud.bash` - Setup for Claude Code cloud environments, run from an environment's setup script with `curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash`. Installs the Linux CLI and daemon into `~/.shelltime/bin`, runs `shelltime cc install`, and adds a Claude Code SessionStart hook (`~/.shelltime/bin/cloud-session-start.sh`, embedded in the script) that signs in with `SHELLTIME_TOKEN` and starts the daemon in each session. A setup script that exits non-zero stops sessions from starting, so it always exits 0 (its `EXIT` trap reports a failure instead), and nothing runs before the final `main` call, so a truncated download installs nothing. The guide that documents it is `content/blogs/track-claude-code-cloud-sessions/` in the web repo; keep the two in step +- `cloud.bash` - Setup for Claude Code cloud environments, run from an environment's setup script with `curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash`. Installs the Linux CLI and daemon into `~/.shelltime/bin`, runs `shelltime cc install`, and adds a Claude Code SessionStart hook (`~/.shelltime/bin/cloud-session-start.sh`, embedded in the script) that signs in with `SHELLTIME_TOKEN` and starts the daemon in each session. It installs nothing unless `CLAUDE_CODE_REMOTE` is `true`, which cloud environments set (the setup script inherits it from `environment-manager`) and which is never true on a user's own machine. A setup script that exits non-zero stops sessions from starting, so it always exits 0 (its `EXIT` trap reports a failure instead), and nothing runs before the final `main` call, so a truncated download installs nothing. The guide that documents it is `content/blogs/track-claude-code-cloud-sessions/` in the web repo; keep the two in step - `hooks/` - Shell-specific hook scripts that track command execution: - `zsh.zsh` - Uses zsh's native `preexec` and `precmd` hooks - `bash.bash` - Uses `bash-preexec` library for preexec/precmd functionality @@ -29,7 +29,7 @@ The CI verifies: 2. Shell configuration files are properly sourced 3. Hook files are placed in `~/.shelltime/hooks/` -The `test-cloud` job runs `cloud.bash` twice on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, that the SessionStart hook is registered exactly once, and that the hook prints nothing without `SHELLTIME_TOKEN`. +The `test-cloud` job checks that `cloud.bash` installs nothing without `CLAUDE_CODE_REMOTE=true`, then runs it twice with it on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, that the SessionStart hook is registered exactly once, and that the hook prints nothing without `SHELLTIME_TOKEN`. ## How Shell Hooks Work diff --git a/README.md b/README.md index f8d0cea..5894a73 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To track Claude Code sessions that run in the cloud ([claude.ai/code](https://cl curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash ``` -The environment also needs your ShellTime API token as `SHELLTIME_TOKEN` in its environment variables, and `api.shelltime.xyz` allowed by its network access. [cloud.bash](cloud.bash) installs the CLI and daemon, connects Claude Code, and adds a SessionStart hook that signs in with the token and starts the daemon in every session. It never fails the setup script. The full guide: [Track your Claude Code cloud sessions](https://shelltime.xyz/blogs/track-claude-code-cloud-sessions). +The environment also needs your ShellTime API token as `SHELLTIME_TOKEN` in its environment variables, and `api.shelltime.xyz` allowed by its network access. [cloud.bash](cloud.bash) installs the CLI and daemon, connects Claude Code, and adds a SessionStart hook that signs in with the token and starts the daemon in every session. It never fails the setup script, and outside a cloud environment (where `CLAUDE_CODE_REMOTE` isn't `true`) it installs nothing. The full guide: [Track your Claude Code cloud sessions](https://shelltime.xyz/blogs/track-claude-code-cloud-sessions). ## What Next? diff --git a/cloud.bash b/cloud.bash index 997fcec..e58f290 100755 --- a/cloud.bash +++ b/cloud.bash @@ -9,7 +9,8 @@ # The environment also needs SHELLTIME_TOKEN in its environment variables and # api.shelltime.xyz allowed by its network access. Setup scripts don't see the # environment's variables, so this only installs; a SessionStart hook signs in -# with the token in every session. +# with the token in every session. Anywhere CLAUDE_CODE_REMOTE isn't true, it +# installs nothing. # # Guide: https://shelltime.xyz/blogs/track-claude-code-cloud-sessions @@ -87,8 +88,10 @@ EOF } main() { - if [ "$(uname -s)" != "Linux" ]; then - echo "cloud.bash is for Claude Code cloud environments; on your own machine run: curl -sSL https://shelltime.xyz/i | bash" >&2 + # Cloud environments set CLAUDE_CODE_REMOTE=true, and it's never true on + # your own machine, where this would swap in Linux binaries and add a hook + if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + echo "cloud.bash only runs in Claude Code cloud environments (CLAUDE_CODE_REMOTE=true); on your own machine run: curl -sSL https://shelltime.xyz/i | bash" >&2 return 1 fi