cc-tracer 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Claude Code Tracer Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-tracer
3
+ Version: 0.1.3
4
+ Summary: A local, infra-free, raw-fidelity inspector for a single Claude Code session — a DevTools Network tab for Claude Code.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 The Claude Code Tracer Authors
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://github.com/zhluo/claude-code-tracer
28
+ Keywords: claude,claude-code,tracing,debugging,observability
29
+ Classifier: Development Status :: 4 - Beta
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Environment :: Console
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Topic :: Software Development :: Debuggers
40
+ Requires-Python: >=3.9
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Requires-Dist: fastapi
44
+ Requires-Dist: uvicorn
45
+ Requires-Dist: httpx
46
+ Dynamic: license-file
47
+
48
+ # cc-tracer (For Claude Code)
49
+
50
+ A local, infra-free, raw-fidelity inspector for a **single** Claude Code session — a
51
+ *DevTools Network tab for Claude Code*. Install it, point Claude Code at it, and watch
52
+ one session's hook events and raw API turns on a live timeline.
53
+
54
+ ## How it captures
55
+
56
+ A local FastAPI server (127.0.0.1:7355) with Two capture tiers:
57
+
58
+ - **Hook tier** — Claude Code hooks POST to the tracer server. Captures *what Claude
59
+ did*: prompts, Pre/PostToolUse, results, stop reasons.
60
+ - **API-proxy tier** — point `ANTHROPIC_BASE_URL` at the tracer; it streams `/v1/*` to
61
+ the real API (plain HTTP in, real HTTPS out — no cert trust needed) and reassembles
62
+ the streaming response into full API turns: system prompt, message context, reasoning
63
+ text, token usage — *what Claude saw and thought*.
64
+
65
+ ## Getting started
66
+
67
+ Prerequisites: `python3` (3.9+), `pip`, and the `claude` CLI on your `PATH`.
68
+
69
+ Install the package, then let one command do everything — configure the Claude Code
70
+ hooks, start the tracer server, and launch Claude Code routed through it:
71
+
72
+ ```bash
73
+ pip install cc-tracer # or: pip install -e . (from a clone)
74
+ cc-tracer start
75
+ ```
76
+
77
+ Then open the tracer UI at <http://127.0.0.1:7355> and use Claude Code as usual. The
78
+ tracer **keeps running after you quit Claude Code** so you can keep browsing the
79
+ capture — run `cc-tracer stop` when you're done.
80
+
81
+ What `cc-tracer start` does:
82
+
83
+ 1. Merges the tracer hooks into the **project-local** `.claude/settings.json` (in the
84
+ current directory, so they apply only to this project — not every Claude session;
85
+ idempotent, backs the original up to `.bak` once). Use `--settings PATH` to target a
86
+ different file, e.g. `~/.claude/settings.json` to trace globally.
87
+ 2. Starts a detached tracer server on `:7355` (hook sink + UI + API proxy), or reuses
88
+ one already running there. The server is left running when Claude exits.
89
+ 3. Exports ANTHROPIC_BASE_URL=http://127.0.0.1:7355 and runs `claude` (you can pass claude args after `--`).
90
+
91
+ Session JSONL is written to `~/.cc-tracer/logs/` (override with `TRACER_LOG_DIR` or
92
+ `--log-dir`).
93
+
94
+ For instance:
95
+ ```bash
96
+ cc-tracer start \
97
+ --log-dir ./logs/ \
98
+ -- -c # -c to continue recent claude session
99
+ ```
100
+
101
+ To run **just the server** without launching Claude — e.g. to browse past captures in
102
+ the UI — use
103
+ ```bash
104
+ cc-tracer start --server-only
105
+ ```
106
+
107
+ ### Stopping
108
+
109
+ ```bash
110
+ cc-tracer stop # stop the server AND remove the tracer's hooks
111
+ ```
112
+
113
+ Hooks follow the server's lifecycle: `start` adds them and `stop` removes only the
114
+ tracer's own entries (your other hooks are left alone). With the server running, you
115
+ can also point a Claude session you launch yourself at it with
116
+ `export ANTHROPIC_BASE_URL=http://127.0.0.1:7355`.
117
+
118
+ ## When to use this vs. OpenTelemetry
119
+
120
+ Claude Code emits OpenTelemetry natively, and tools like SigNoz, Grafana, and
121
+ LangSmith build on it. They are **aggregate observability** — dashboards, cost/latency
122
+ trends, alerting, cross-session correlation across a fleet. If that's your goal, use
123
+ them; this tracer doesn't try to.
124
+
125
+ This tracer targets the thing those tools explicitly aren't: a **local, infra-free,
126
+ raw-fidelity inspector for a single session** — think "DevTools Network tab for Claude
127
+ Code."
128
+
129
+ | | This tracer | Native OTel (SigNoz / Grafana / LangSmith) |
130
+ | --- | --- | --- |
131
+ | Raw API request/response body | ✅ | ❌ (spans/metrics; content redacted by default) |
132
+ | Reasoning / thinking text | ✅ | ❌ |
133
+ | Tool layer (Pre/Post, Edit diffs) | ✅ | ✅ |
134
+ | Backend infra required | none (JSONL + one HTML file) | collector + storage + UI |
135
+ | Data stays fully local | ✅ | configurable / often SaaS |
136
+ | Cross-session stats & alerting | ❌ | ✅ |
137
+
138
+ The closest off-the-shelf analog is a manual mitmproxy setup; the base-URL forwarder
139
+ avoids that approach's CA forging and `NODE_EXTRA_CA_CERTS` fuss.
140
+
141
+ ### Caveats
142
+
143
+ - **Proxy ≠ full coverage.** The API proxy only sees HTTP traffic. Transport that
144
+ isn't plain HTTP (e.g. the Agent SDK's IPC/WebSocket) is captured only at the hook
145
+ tier. Native OTel emits regardless of transport.
146
+ - **SSE reassembly is schema-coupled.** Rebuilding turns depends on the current
147
+ Anthropic event shape, so it needs upkeep when the API evolves — a maintenance cost
148
+ the OTel-based tools don't carry.
@@ -0,0 +1,101 @@
1
+ # cc-tracer (For Claude Code)
2
+
3
+ A local, infra-free, raw-fidelity inspector for a **single** Claude Code session — a
4
+ *DevTools Network tab for Claude Code*. Install it, point Claude Code at it, and watch
5
+ one session's hook events and raw API turns on a live timeline.
6
+
7
+ ## How it captures
8
+
9
+ A local FastAPI server (127.0.0.1:7355) with Two capture tiers:
10
+
11
+ - **Hook tier** — Claude Code hooks POST to the tracer server. Captures *what Claude
12
+ did*: prompts, Pre/PostToolUse, results, stop reasons.
13
+ - **API-proxy tier** — point `ANTHROPIC_BASE_URL` at the tracer; it streams `/v1/*` to
14
+ the real API (plain HTTP in, real HTTPS out — no cert trust needed) and reassembles
15
+ the streaming response into full API turns: system prompt, message context, reasoning
16
+ text, token usage — *what Claude saw and thought*.
17
+
18
+ ## Getting started
19
+
20
+ Prerequisites: `python3` (3.9+), `pip`, and the `claude` CLI on your `PATH`.
21
+
22
+ Install the package, then let one command do everything — configure the Claude Code
23
+ hooks, start the tracer server, and launch Claude Code routed through it:
24
+
25
+ ```bash
26
+ pip install cc-tracer # or: pip install -e . (from a clone)
27
+ cc-tracer start
28
+ ```
29
+
30
+ Then open the tracer UI at <http://127.0.0.1:7355> and use Claude Code as usual. The
31
+ tracer **keeps running after you quit Claude Code** so you can keep browsing the
32
+ capture — run `cc-tracer stop` when you're done.
33
+
34
+ What `cc-tracer start` does:
35
+
36
+ 1. Merges the tracer hooks into the **project-local** `.claude/settings.json` (in the
37
+ current directory, so they apply only to this project — not every Claude session;
38
+ idempotent, backs the original up to `.bak` once). Use `--settings PATH` to target a
39
+ different file, e.g. `~/.claude/settings.json` to trace globally.
40
+ 2. Starts a detached tracer server on `:7355` (hook sink + UI + API proxy), or reuses
41
+ one already running there. The server is left running when Claude exits.
42
+ 3. Exports ANTHROPIC_BASE_URL=http://127.0.0.1:7355 and runs `claude` (you can pass claude args after `--`).
43
+
44
+ Session JSONL is written to `~/.cc-tracer/logs/` (override with `TRACER_LOG_DIR` or
45
+ `--log-dir`).
46
+
47
+ For instance:
48
+ ```bash
49
+ cc-tracer start \
50
+ --log-dir ./logs/ \
51
+ -- -c # -c to continue recent claude session
52
+ ```
53
+
54
+ To run **just the server** without launching Claude — e.g. to browse past captures in
55
+ the UI — use
56
+ ```bash
57
+ cc-tracer start --server-only
58
+ ```
59
+
60
+ ### Stopping
61
+
62
+ ```bash
63
+ cc-tracer stop # stop the server AND remove the tracer's hooks
64
+ ```
65
+
66
+ Hooks follow the server's lifecycle: `start` adds them and `stop` removes only the
67
+ tracer's own entries (your other hooks are left alone). With the server running, you
68
+ can also point a Claude session you launch yourself at it with
69
+ `export ANTHROPIC_BASE_URL=http://127.0.0.1:7355`.
70
+
71
+ ## When to use this vs. OpenTelemetry
72
+
73
+ Claude Code emits OpenTelemetry natively, and tools like SigNoz, Grafana, and
74
+ LangSmith build on it. They are **aggregate observability** — dashboards, cost/latency
75
+ trends, alerting, cross-session correlation across a fleet. If that's your goal, use
76
+ them; this tracer doesn't try to.
77
+
78
+ This tracer targets the thing those tools explicitly aren't: a **local, infra-free,
79
+ raw-fidelity inspector for a single session** — think "DevTools Network tab for Claude
80
+ Code."
81
+
82
+ | | This tracer | Native OTel (SigNoz / Grafana / LangSmith) |
83
+ | --- | --- | --- |
84
+ | Raw API request/response body | ✅ | ❌ (spans/metrics; content redacted by default) |
85
+ | Reasoning / thinking text | ✅ | ❌ |
86
+ | Tool layer (Pre/Post, Edit diffs) | ✅ | ✅ |
87
+ | Backend infra required | none (JSONL + one HTML file) | collector + storage + UI |
88
+ | Data stays fully local | ✅ | configurable / often SaaS |
89
+ | Cross-session stats & alerting | ❌ | ✅ |
90
+
91
+ The closest off-the-shelf analog is a manual mitmproxy setup; the base-URL forwarder
92
+ avoids that approach's CA forging and `NODE_EXTRA_CA_CERTS` fuss.
93
+
94
+ ### Caveats
95
+
96
+ - **Proxy ≠ full coverage.** The API proxy only sees HTTP traffic. Transport that
97
+ isn't plain HTTP (e.g. the Agent SDK's IPC/WebSocket) is captured only at the hook
98
+ tier. Native OTel emits regardless of transport.
99
+ - **SSE reassembly is schema-coupled.** Rebuilding turns depends on the current
100
+ Anthropic event shape, so it needs upkeep when the API evolves — a maintenance cost
101
+ the OTel-based tools don't carry.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cc-tracer"
7
+ version = "0.1.3"
8
+ description = "A local, infra-free, raw-fidelity inspector for a single Claude Code session — a DevTools Network tab for Claude Code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ keywords = ["claude", "claude-code", "tracing", "debugging", "observability"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Environment :: Console",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Debuggers",
25
+ ]
26
+ dependencies = ["fastapi", "uvicorn", "httpx"]
27
+
28
+ [project.scripts]
29
+ cc-tracer = "cc_tracer.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/zhluo/claude-code-tracer"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ cc_tracer = ["static/*", "settings_example.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """cc-tracer — a local, infra-free inspector for a single Claude Code session."""
2
+
3
+ __version__ = "0.1.3"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,194 @@
1
+ """cc-tracer command-line interface.
2
+
3
+ start install hooks (if needed), start a detached server, run Claude through it
4
+ stop stop the server and remove its hooks
5
+
6
+ (`_serve` is an internal, hidden command: the bare server process that `start`
7
+ spawns detached. Use `start` instead.)
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+ import signal
13
+ import socket
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+
19
+ from . import config, hooks as hooks_mod
20
+
21
+ # Project-local by default so hooks only apply to Claude Code run in this project
22
+ # (not every session on the machine). Override with --settings ~/.claude/settings.json.
23
+ DEFAULT_SETTINGS = ".claude/settings.json"
24
+
25
+
26
+ def _wait_port(port, timeout=10.0):
27
+ deadline = time.time() + timeout
28
+ while time.time() < deadline:
29
+ if _port_open(port):
30
+ return True
31
+ time.sleep(0.1)
32
+ return False
33
+
34
+
35
+ def _port_open(port):
36
+ with socket.socket() as s:
37
+ s.settimeout(0.2)
38
+ try:
39
+ s.connect(("127.0.0.1", port))
40
+ return True
41
+ except OSError:
42
+ return False
43
+
44
+
45
+ def _hook_url(port):
46
+ return f"http://127.0.0.1:{port}/event"
47
+
48
+
49
+ def cmd_serve(a):
50
+ """Internal `_serve`: the bare server process (writes a pidfile so `stop`
51
+ can find it). Spawned detached by `start`; not meant to be run directly."""
52
+ if a.log_dir:
53
+ os.environ["TRACER_LOG_DIR"] = str(a.log_dir)
54
+ import uvicorn
55
+ from .server import app # imported after env is set so LOG_DIR picks it up
56
+ pid_file = config.pid_file(a.port)
57
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
58
+ pid_file.write_text(str(os.getpid()))
59
+ try:
60
+ uvicorn.run(app, host=a.host, port=a.port)
61
+ finally:
62
+ try:
63
+ pid_file.unlink()
64
+ except OSError:
65
+ pass
66
+ return 0
67
+
68
+
69
+ def cmd_start(a):
70
+ base = f"http://127.0.0.1:{a.port}"
71
+ settings = Path(a.settings).expanduser()
72
+ log_dir = Path(a.log_dir) if a.log_dir else config.log_dir()
73
+ log_dir.mkdir(parents=True, exist_ok=True)
74
+
75
+ # 1. Ensure the tracer hooks are installed (idempotent).
76
+ added = hooks_mod.install(settings, _hook_url(a.port))
77
+ print(f"Hooks {'installed in' if added else 'already in'} {settings}")
78
+
79
+ # 2. Start a detached server (or reuse one already on the port). Detached +
80
+ # own session so quitting Claude leaves the tracer running.
81
+ server = None
82
+ if _port_open(a.port):
83
+ print(f"Reusing tracer already running at {base}")
84
+ else:
85
+ env = os.environ.copy()
86
+ env["TRACER_LOG_DIR"] = str(log_dir)
87
+ server_log = open(log_dir / "server.log", "a")
88
+ server = subprocess.Popen(
89
+ [sys.executable, "-m", "cc_tracer", "_serve", "--host", "127.0.0.1", "--port", str(a.port)],
90
+ env=env, stdout=server_log, stderr=server_log, start_new_session=True,
91
+ )
92
+ if not _wait_port(a.port):
93
+ print(f"cc-tracer server failed to start; see {log_dir / 'server.log'}", file=sys.stderr)
94
+ server.terminate()
95
+ return 1
96
+ print(f"Started tracer at {base} (log: {log_dir / 'server.log'})")
97
+
98
+ print(f"Tracer UI: {base}")
99
+ stop = "cc-tracer stop" + ("" if a.port == config.DEFAULT_PORT else f" --port {a.port}")
100
+
101
+ # 3. With --server-only, leave the detached server running and exit.
102
+ if a.server_only:
103
+ print(f" point Claude at it: export ANTHROPIC_BASE_URL={base}")
104
+ print(f" stop it (and remove hooks) with: {stop}")
105
+ return 0
106
+
107
+ # Otherwise run Claude Code routed through the tracer.
108
+ run_env = os.environ.copy()
109
+ run_env["ANTHROPIC_BASE_URL"] = base
110
+ claude_args = [x for x in a.claude_args if x != "--"]
111
+ try:
112
+ rc = subprocess.call(["claude", *claude_args], env=run_env)
113
+ except FileNotFoundError:
114
+ print("claude not found on PATH. The tracer is running; start Claude yourself "
115
+ f"with: export ANTHROPIC_BASE_URL={base}", file=sys.stderr)
116
+ rc = 127
117
+ except KeyboardInterrupt:
118
+ rc = 130
119
+
120
+ # Leave the server running after Claude exits; `stop` tears it down.
121
+ print(f"\nTracer still running at {base}")
122
+ print(f" stop it (and remove hooks) with: {stop}")
123
+ return rc
124
+
125
+
126
+ def cmd_stop(a):
127
+ if a.log_dir:
128
+ os.environ["TRACER_LOG_DIR"] = str(a.log_dir)
129
+
130
+ # 1. Stop the server (via its pidfile), if one is running.
131
+ pid_file = config.pid_file(a.port)
132
+ if pid_file.exists():
133
+ try:
134
+ pid = int(pid_file.read_text().strip())
135
+ except ValueError:
136
+ print(f"Corrupt pidfile; removing {pid_file}")
137
+ pid_file.unlink()
138
+ pid = None
139
+ if pid is not None:
140
+ try:
141
+ os.kill(pid, signal.SIGTERM)
142
+ for _ in range(50): # wait for shutdown to free the port
143
+ if not _port_open(a.port):
144
+ break
145
+ time.sleep(0.1)
146
+ print(f"Stopped cc-tracer (pid {pid}) on port {a.port}.")
147
+ except ProcessLookupError:
148
+ print(f"Tracer (pid {pid}) wasn't running.")
149
+ try:
150
+ pid_file.unlink()
151
+ except OSError:
152
+ pass
153
+ else:
154
+ msg = f"No running tracer for port {a.port}."
155
+ if _port_open(a.port):
156
+ msg += f" (Something else is listening on {a.port}.)"
157
+ print(msg)
158
+
159
+ # 2. Remove the tracer hooks (only the entries pointing at our URL).
160
+ settings = Path(a.settings).expanduser()
161
+ url = _hook_url(a.port)
162
+ removed = hooks_mod.uninstall(settings, url)
163
+ print(f"{'Removed' if removed else 'No'} tracer hooks ({url}) in {settings}")
164
+ return 0
165
+
166
+
167
+ def main(argv=None):
168
+ p = argparse.ArgumentParser(prog="cc-tracer", description="A local inspector for a single Claude Code session.")
169
+ sub = p.add_subparsers(dest="cmd", required=True)
170
+
171
+ s = sub.add_parser("start", help="install hooks, start a detached server, run Claude through it")
172
+ s.add_argument("--port", type=int, default=config.DEFAULT_PORT)
173
+ s.add_argument("--settings", default=DEFAULT_SETTINGS, help="settings.json for hooks (default: project-local .claude/settings.json)")
174
+ s.add_argument("--log-dir", type=Path, help="override $TRACER_LOG_DIR")
175
+ s.add_argument("--server-only", action="store_true",
176
+ help="just start the server (don't launch Claude)")
177
+ s.add_argument("claude_args", nargs=argparse.REMAINDER, help="args passed through to `claude`")
178
+ s.set_defaults(func=cmd_start)
179
+
180
+ st = sub.add_parser("stop", help="stop the tracer server and remove its hooks")
181
+ st.add_argument("--port", type=int, default=config.DEFAULT_PORT)
182
+ st.add_argument("--settings", default=DEFAULT_SETTINGS, help="settings.json to remove hooks from (default: project-local)")
183
+ st.add_argument("--log-dir", type=Path, help="where start recorded its pidfile")
184
+ st.set_defaults(func=cmd_stop)
185
+
186
+ # Internal: the bare server process that `start` spawns detached.
187
+ sv = sub.add_parser("_serve", help=argparse.SUPPRESS)
188
+ sv.add_argument("--port", type=int, default=config.DEFAULT_PORT)
189
+ sv.add_argument("--host", default="127.0.0.1")
190
+ sv.add_argument("--log-dir", type=Path)
191
+ sv.set_defaults(func=cmd_serve)
192
+
193
+ a = p.parse_args(argv)
194
+ return a.func(a) or 0
@@ -0,0 +1,17 @@
1
+ """Shared configuration: where session JSONL lives, default port."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ DEFAULT_PORT = 7355
7
+
8
+
9
+ def log_dir() -> Path:
10
+ """Directory holding per-session JSONL. Override with $TRACER_LOG_DIR;
11
+ defaults to ~/.cc-tracer/logs so sessions land in one predictable place."""
12
+ return Path(os.environ.get("TRACER_LOG_DIR") or Path.home() / ".cc-tracer" / "logs")
13
+
14
+
15
+ def pid_file(port: int) -> Path:
16
+ """Where `serve` records its PID so `stop` can find it (per port, in log_dir)."""
17
+ return log_dir() / f"cc-tracer-{port}.pid"
@@ -0,0 +1,70 @@
1
+ """Install / remove the tracer's HTTP hooks in a Claude Code settings.json."""
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ # The Claude Code events we capture. The server records any event it receives,
8
+ # so this list is purely which hooks we register.
9
+ EVENTS = [
10
+ "SessionStart", "SessionEnd", "UserPromptSubmit",
11
+ "PreToolUse", "PostToolUse", "PostToolUseFailure",
12
+ "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "Stop",
13
+ ]
14
+
15
+
16
+ def tracer_hooks(url):
17
+ """The hooks block that POSTs every captured event to `url`."""
18
+ return {ev: [{"hooks": [{"type": "http", "url": url}]}] for ev in EVENTS}
19
+
20
+
21
+ def install(settings_path, url):
22
+ """Merge the tracer hooks into settings.json (idempotent). Backs the original
23
+ up to <name>.json.bak once. Returns True if anything was added."""
24
+ settings_path = Path(settings_path)
25
+ current = {}
26
+ if settings_path.exists():
27
+ current = json.loads(settings_path.read_text() or "{}")
28
+ bak = settings_path.with_suffix(".json.bak")
29
+ if not bak.exists():
30
+ shutil.copy(settings_path, bak)
31
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
32
+
33
+ hooks = current.setdefault("hooks", {})
34
+ added = False
35
+ for event, entries in tracer_hooks(url).items():
36
+ bucket = hooks.setdefault(event, [])
37
+ serialized = json.dumps(bucket)
38
+ for entry in entries:
39
+ if json.dumps(entry) not in serialized:
40
+ bucket.append(entry)
41
+ added = True
42
+ settings_path.write_text(json.dumps(current, indent=2) + "\n")
43
+ return added
44
+
45
+
46
+ def uninstall(settings_path, url):
47
+ """Remove only the HTTP hook entries pointing at `url`. Returns True if any
48
+ were removed. Leaves the user's other hooks untouched."""
49
+ settings_path = Path(settings_path)
50
+ if not settings_path.exists():
51
+ return False
52
+ current = json.loads(settings_path.read_text() or "{}")
53
+ hooks = current.get("hooks", {})
54
+ removed = False
55
+ for event in list(hooks):
56
+ kept_groups = []
57
+ for group in hooks[event]:
58
+ inner = [h for h in group.get("hooks", [])
59
+ if not (h.get("type") == "http" and h.get("url") == url)]
60
+ if len(inner) != len(group.get("hooks", [])):
61
+ removed = True
62
+ if inner:
63
+ kept_groups.append({**group, "hooks": inner})
64
+ if kept_groups:
65
+ hooks[event] = kept_groups
66
+ else:
67
+ del hooks[event]
68
+ current["hooks"] = hooks
69
+ settings_path.write_text(json.dumps(current, indent=2) + "\n")
70
+ return removed