Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,48 @@ 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: 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
# 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

- 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; }
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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
Expand All @@ -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 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

Each shell hook script:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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?

Once the installation script finishes successfully, you should be able to track your shell time.
Expand Down
106 changes: 106 additions & 0 deletions cloud.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/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. Anywhere CLAUDE_CODE_REMOTE isn't true, it
# installs nothing.
#
# 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 </dev/null &
fi
exit 0
EOF
chmod 0755 "$HOOK_PATH"

# Replaces the hook an earlier run added instead of adding a second one
jq --arg cmd "$HOOK_PATH" '
.hooks.SessionStart = [(.hooks.SessionStart // [])[] | select(all(.hooks[]?; .command != $cmd))]
+ [{hooks: [{type: "command", command: $cmd}]}]
' "$SETTINGS_PATH" >"$SETTINGS_PATH.tmp"
mv "$SETTINGS_PATH.tmp" "$SETTINGS_PATH"
}

main() {
# 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

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 "$@"
Loading