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
17 changes: 16 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ jobs:
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
config="$HOME/.shelltime/config.yaml"
daemon="$HOME/.shelltime/bin/shelltime-daemon"
# The hook prints nothing: a SessionStart hook's output goes into Claude's context
# Without SHELLTIME_TOKEN it signs in with the network secret's placeholder
out=$(env -u SHELLTIME_TOKEN "$hook")
[ -z "$out" ] || { echo "hook printed: $out"; exit 1; }
grep -qx "token: proxy-injected" "$config" || { echo "expected the proxy-injected token"; cat "$config"; exit 1; }
# and starts the daemon, once
for _ in 1 2 3 4 5; do pgrep -fx "$daemon" >/dev/null && break; sleep 1; done
pgrep -fx "$daemon" >/dev/null || { echo "daemon not started"; cat "$HOME/.shelltime/logs/shelltime-daemon.log"; exit 1; }
# SHELLTIME_TOKEN takes the placeholder's place, and back again once it's gone
out=$(SHELLTIME_TOKEN=st_v1_ci_token "$hook")
[ -z "$out" ] || { echo "hook printed: $out"; exit 1; }
grep -qx "token: st_v1_ci_token" "$config" || { echo "expected SHELLTIME_TOKEN"; cat "$config"; exit 1; }
env -u SHELLTIME_TOKEN "$hook"
grep -qx "token: proxy-injected" "$config" || { echo "expected the proxy-injected token again"; cat "$config"; exit 1; }
[ "$(pgrep -fxc "$daemon")" = 1 ] || { echo "expected one daemon"; pgrep -fxa "$daemon"; exit 1; }
pkill -fx "$daemon"
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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
- `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 and starts the daemon in each session. The token is a network secret of the environment (host `api.shelltime.xyz`, header `Authorization`, prefix `CLI`) that Anthropic's agent proxy adds to requests outside the VM, so the hook signs in with the placeholder `proxy-injected` (the CLI then sends no `Authorization` header; needs a CLI release with `model.ProxyInjectedToken`). `SHELLTIME_TOKEN` in the environment variables, for environments without network secrets, replaces the placeholder. Setup scripts get neither. 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 @@ -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 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`.
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, signs in with `proxy-injected` (or `SHELLTIME_TOKEN` when set) and starts one daemon.

## How Shell Hooks Work

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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).
Then add your ShellTime API token to the environment as a network secret: allowed website `api.shelltime.xyz`, header `Authorization`, prefix `CLI`. Anthropic's agent proxy adds it to the daemon's requests outside the session's VM, so sessions never see the token, and the host needs no network access setting. [cloud.bash](cloud.bash) installs the CLI and daemon, connects Claude Code, and adds a SessionStart hook that signs in and starts the daemon in every session. An environment without network secrets can hold the token as `SHELLTIME_TOKEN` in its environment variables instead, with `api.shelltime.xyz` allowed by its network access; the hook uses it when it's set. 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?

Expand Down
31 changes: 19 additions & 12 deletions cloud.bash
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
#
# 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.
# Then add your ShellTime API token to the environment as a network secret for
# api.shelltime.xyz, sent in the Authorization header with the prefix CLI.
# Anthropic's agent proxy adds it to the daemon's requests after they leave the
# session's VM, so the token is never in the VM, and the secret's host needs no
# network access setting. An environment without network secrets can hold the
# token in SHELLTIME_TOKEN among its environment variables instead, with
# api.shelltime.xyz allowed by its network access. Setup scripts get neither,
# so this only installs; a SessionStart hook signs in and starts the daemon in
# every session. Anywhere CLAUDE_CODE_REMOTE isn't true, it installs nothing.
#
# Guide: https://shelltime.xyz/blogs/track-claude-code-cloud-sessions

Expand Down Expand Up @@ -59,18 +63,21 @@ connect_claude_code() {
}

# 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
# sessions see 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
# With the token in a network secret, the CLI signs in with the placeholder
# proxy-injected and sends no Authorization header; the agent proxy adds the
# real one. SHELLTIME_TOKEN, for environments without network secrets, wins.
token="${SHELLTIME_TOKEN:-proxy-injected}"
# The first session, or the token changed in the environment settings
if ! grep -qF "$token" "$st/config.yaml" 2>/dev/null; then
"$st/bin/shelltime" auth --token "$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 &
Expand Down Expand Up @@ -99,7 +106,7 @@ main() {
install_cli
connect_claude_code
install_session_hook
echo "ShellTime is installed; Claude Code sessions will be tracked once SHELLTIME_TOKEN is set"
echo "ShellTime is installed; Claude Code sessions will be tracked once your ShellTime token is a network secret for api.shelltime.xyz"
}

# Nothing runs before this line, so a download cut short installs nothing
Expand Down
Loading