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
9 changes: 9 additions & 0 deletions commands/cc.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,19 @@ func commandCCInstall(c *cli.Context) error {

color.Green.Println("Claude Code OTEL configuration has been installed!")
color.Yellow.Println("Please restart your Claude Code sessions (terminal, desktop app, IDE) to apply changes.")
printCCPrivacyNote(settingsService.SettingsPath())

return nil
}

// printCCPrivacyNote says what the installed config sends beyond token and cost counts.
func printCCPrivacyNote(settingsPath string) {
color.Gray.Println("Privacy: besides usage and cost, Claude Code now sends your prompts, tool details (Bash")
color.Gray.Println("commands, file paths, truncated tool input, tool errors) and assistant responses to your")
color.Gray.Printf("ShellTime account. To keep some of them out, set OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_DETAILS or\n")
color.Gray.Printf("OTEL_LOG_ASSISTANT_RESPONSES to 0 in the env of %s (re-running install resets them).\n", settingsPath)
}

func commandCCUninstall(c *cli.Context) error {
color.Yellow.Println("Removing Claude Code OTEL configuration...")

Expand Down
21 changes: 21 additions & 0 deletions commands/cc_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package commands

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/gookit/color"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -135,3 +137,22 @@ func TestCCInstall_IdempotentNoDuplicateKeys(t *testing.T) {
assert.Equal(t, string(first), string(second), "install should be idempotent")
assert.Equal(t, 1, strings.Count(string(second), "OTEL_EXPORTER_OTLP_ENDPOINT"))
}

func TestInstallPrivacyNotes(t *testing.T) {
var buf bytes.Buffer
color.SetOutput(&buf)
t.Cleanup(color.ResetOutput)

printCCPrivacyNote("/home/u/.claude/settings.json")
cc := buf.String()
for _, want := range []string{"Bash", "file paths", "tool input", "tool errors", "assistant responses", "OTEL_LOG_TOOL_DETAILS", "OTEL_LOG_ASSISTANT_RESPONSES", "/home/u/.claude/settings.json"} {
assert.Contains(t, cc, want)
}

buf.Reset()
printCodexPrivacyNote()
codex := buf.String()
for _, want := range []string{"shell commands", "tool output", "responses", "otel.log_agent_responses", "~/.codex/config.toml"} {
assert.Contains(t, codex, want)
}
}
9 changes: 9 additions & 0 deletions commands/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,19 @@ func commandCodexInstall(c *cli.Context) error {

color.Green.Println("Codex OTEL configuration has been installed to ~/.codex/config.toml")
color.Yellow.Println("The Codex CLI will now send telemetry to ShellTime daemon.")
printCodexPrivacyNote()

return nil
}

// printCodexPrivacyNote says what the installed config sends beyond token counts.
func printCodexPrivacyNote() {
color.Gray.Println("Privacy: besides usage, Codex now sends your prompts, tool details (shell commands, patches")
color.Gray.Println("and other tool arguments, truncated tool output, tool errors) and its final responses to your")
color.Gray.Println("ShellTime account. To keep prompts or responses out, set otel.log_user_prompt or")
color.Gray.Println("otel.log_agent_responses to false in ~/.codex/config.toml (re-running install resets them).")
}

func commandCodexUninstall(c *cli.Context) error {
color.Yellow.Println("Removing Codex OTEL configuration...")

Expand Down
25 changes: 25 additions & 0 deletions commands/doctor_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,18 @@ func doctorCheckClaude(env *doctorEnv) []doctorResult {
} else {
env.claudeOtel = true
results = append(results, doctorResult{ID: "claude.otel", Status: doctorOK, Message: fmt.Sprintf("Claude Code reports usage to shelltime (%s).", svc.SettingsPath())})

// Settings written by an older `cc install` lack the newer keys (tool details,
// assistant responses, delta temporality, ...).
if missing, err := svc.MissingManagedKeys(); err == nil && len(missing) > 0 {
results = append(results, doctorResult{
ID: "claude.otel_keys",
Status: doctorWarn,
Message: fmt.Sprintf("Claude Code's shelltime OTEL config is out of date (missing %s), so some session details aren't reported.", strings.Join(missing, ", ")),
Fix: "Run `shelltime cc install` again, then restart Claude Code.",
AutoFix: fix,
})
}
}

var legacyShells []string
Expand Down Expand Up @@ -839,6 +851,19 @@ func doctorCheckCodex(env *doctorEnv) []doctorResult {
}
results := []doctorResult{result}

// A config written by an older `codex install` lacks log_agent_responses.
if env.codexOtel {
if missing, err := model.NewCodexOtelConfigService().MissingManagedKeys(); err == nil && len(missing) > 0 {
results = append(results, doctorResult{
ID: "codex.otel_keys",
Status: doctorWarn,
Message: fmt.Sprintf("Codex's shelltime OTEL config is out of date (missing otel.%s), so some session details aren't reported.", strings.Join(missing, ", otel.")),
Fix: "Run `shelltime codex install` again, then restart Codex.",
AutoFix: fix,
})
}
}

if ok, _ := doctorCodexInstallationStatus(); !ok {
results = append(results, doctorResult{
ID: "codex.auth",
Expand Down
28 changes: 27 additions & 1 deletion commands/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,19 @@ func TestDoctorCheckClaude(t *testing.T) {
assert.True(t, env.claudeOtel)
assert.False(t, hasDoctorResult(results, "claude.legacy_env"))

// Settings from an older `cc install` lack the newer keys: warn and offer to re-run install.
outdated := findDoctorResult(t, results, "claude.otel_keys")
assert.Equal(t, doctorWarn, outdated.Status)
assert.Contains(t, outdated.Message, "OTEL_LOG_TOOL_DETAILS")
assert.Contains(t, outdated.Fix, "shelltime cc install")
require.NotNil(t, outdated.AutoFix)
assert.Equal(t, "claude.install", outdated.AutoFix.Key)

require.NoError(t, model.NewClaudeSettingsAICodeOtelEnvService().Install())
results = doctorCheckClaude(env)
assert.Equal(t, doctorOK, findDoctorResult(t, results, "claude.otel").Status)
assert.False(t, hasDoctorResult(results, "claude.otel_keys"), "a current install has every key")

// An OTEL block written to ~/.zshrc by older versions is flagged for migration.
require.NoError(t, os.WriteFile(filepath.Join(home, ".zshrc"), nil, 0644))
require.NoError(t, model.NewZshAICodeOtelEnvService().Install())
Expand Down Expand Up @@ -557,12 +570,25 @@ func TestDoctorCheckCodex(t *testing.T) {
assert.Equal(t, doctorFail, otelResult.Status)
assert.Nil(t, otelResult.AutoFix, "codex install can't fix invalid TOML")

// A config from an older `codex install` reports usage but lacks log_agent_responses.
older := "[otel]\nlog_user_prompt = true\n\n[otel.exporter.otlp-grpc]\nendpoint = \"" + model.AICodeOtelEndpoint + "\"\n"
require.NoError(t, os.WriteFile(configPath, []byte(older), 0644))
results := doctorCheckCodex(env)
assert.Equal(t, doctorOK, findDoctorResult(t, results, "codex.otel").Status)
outdated := findDoctorResult(t, results, "codex.otel_keys")
assert.Equal(t, doctorWarn, outdated.Status)
assert.Contains(t, outdated.Message, "otel.log_agent_responses")
require.NotNil(t, outdated.AutoFix)
assert.Equal(t, "codex.install", outdated.AutoFix.Key)

require.NoError(t, os.Remove(configPath))
env.codexOtel = false
require.NoError(t, model.NewCodexOtelConfigService().Install())
results := doctorCheckCodex(env)
results = doctorCheckCodex(env)
assert.Equal(t, doctorOK, findDoctorResult(t, results, "codex.otel").Status)
assert.True(t, env.codexOtel)
assert.False(t, hasDoctorResult(results, "codex.auth"))
assert.False(t, hasDoctorResult(results, "codex.otel_keys"))

doctorCodexInstallationStatus = daemon.CodexInstallationStatus
assert.Equal(t, doctorInfo, findDoctorResult(t, doctorCheckCodex(env), "codex.auth").Status, "no auth.json")
Expand Down
Loading
Loading