{"id":50425621,"url":"https://github.com/tywalch/llm-cmd-logger","last_synced_at":"2026-05-31T10:30:32.386Z","repository":{"id":350039387,"uuid":"1204926290","full_name":"tywalch/llm-cmd-logger","owner":"tywalch","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-08T15:42:05.000Z","size":20,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-08T17:30:09.806Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tywalch.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-08T13:19:01.000Z","updated_at":"2026-04-08T15:44:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tywalch/llm-cmd-logger","commit_stats":null,"previous_names":["tywalch/llm-cmd-logger"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/tywalch/llm-cmd-logger","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tywalch%2Fllm-cmd-logger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tywalch%2Fllm-cmd-logger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tywalch%2Fllm-cmd-logger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tywalch%2Fllm-cmd-logger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tywalch","download_url":"https://codeload.github.com/tywalch/llm-cmd-logger/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tywalch%2Fllm-cmd-logger/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33728391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-31T10:30:31.673Z","updated_at":"2026-05-31T10:30:32.373Z","avatar_url":"https://github.com/tywalch.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cmd-log\n\nUnified command logger for AI coding agents. Captures every shell command executed by **Claude Code**, **ChatGPT/Codex**, and **Cursor** into monthly log files with a consistent format.\n\nEach tool has its own hook system with different payload shapes and configuration files. This project provides a single TypeScript script that auto-detects the source from the payload structure, extracts the relevant fields, and appends a formatted log line.\n\n## Quick start\n\n```bash\n# Install dependencies and build\nnpm install\nnpm run build\n\n# Register hooks in all three tools (idempotent — safe to re-run)\nnpm run install-hooks\n\n# Restart Claude Code, Codex, and Cursor for hooks to take effect\n```\n\nLogs will appear in `./logs/YYYY-MM.log` as commands are executed.\n\n## Project structure\n\n```\ncmd-log/\n  src/\n    index.ts        # Main logging script — reads stdin, classifies, logs\n    install.ts      # Idempotent hook installer for all three tools\n  dist/             # Compiled JavaScript output (generated by `npm run build`)\n  logs/             # Monthly log files (e.g. 2026-04.log)\n  archived/         # Legacy v1 scripts and docs (Python, shell)\n  package.json\n  tsconfig.json\n```\n\n## How it works\n\nEach AI tool fires a hook when a shell command is executed. The hook spawns this script and pipes a JSON payload on stdin. The script:\n\n1. Reads JSON from stdin\n2. Classifies the payload into a typed event using a discriminated union\n3. Extracts timestamp, source, model, cwd, and command\n4. Appends a single formatted line to `./logs/YYYY-MM.log`\n\n### Discriminated union\n\nThe core type system uses a discriminated union on the `kind` field:\n\n```typescript\ntype HookEvent = CursorCommand | CodexCommand | ClaudeCommand | ClaudeSession;\n\ninterface CursorCommand  { kind: \"cursor-command\";  command: string; cwd: string; model: string; }\ninterface CodexCommand   { kind: \"codex-command\";   command: string; cwd: string; model: string; }\ninterface ClaudeCommand  { kind: \"claude-command\";   command: string; cwd: string; model: string; }\ninterface ClaudeSession  { kind: \"claude-session\";   model: string; }\n```\n\n`ClaudeSession` is a non-loggable event (no command). The `CommandEvent` subtype excludes it:\n\n```typescript\ntype CommandEvent = CursorCommand | CodexCommand | ClaudeCommand;\n```\n\n### Source detection\n\nThe `classify(raw: unknown)` function inspects the raw JSON to determine which tool sent it. Each tool has a unique payload fingerprint:\n\n| Source | Fingerprint | Hook event |\n|--------|------------|------------|\n| Cursor | top-level `command` + `workspace_roots` array | `afterShellExecution` |\n| Codex/ChatGPT | `tool_input.command` + top-level `model` | `PostToolUse` (Bash) |\n| Claude (session) | `event: \"SessionStart\"` | `SessionStart` |\n| Claude (command) | `tool_input.command` without `model` | `PostToolUse` (Bash) |\n\nDetection order matters. The function checks Cursor first (most distinctive shape), then Codex (has both `tool_input.command` and `model`), then Claude variants.\n\n## Hook payload reference\n\n### Cursor (`afterShellExecution`)\n\nFires after the agent runs a shell command. Passive — no stdout response required.\n\n```json\n{\n  \"command\": \"echo hello\",\n  \"cwd\": \"/Users/you/project\",\n  \"workspace_roots\": [\"/Users/you/project\"],\n  \"model\": \"gpt-5.3\",\n  \"model_name\": \"composer-2-fast\",\n  \"modelId\": \"composer-fast\"\n}\n```\n\n- `command` is always present\n- `cwd` may be empty; falls back to `workspace_roots[0]`\n- Model is extracted with priority: `modelId` \u003e `model_name` \u003e `model` \u003e `\"unknown\"`\n- `workspace_roots` is the key differentiator from other payloads\n\n### ChatGPT / Codex (`PostToolUse`, matcher: `Bash`)\n\nFires after a Bash tool call. Passive — no stdout response required.\n\n```json\n{\n  \"tool_input\": {\n    \"command\": \"go test ./...\"\n  },\n  \"model\": \"gpt-4\",\n  \"cwd\": \"/Users/you/project\"\n}\n```\n\n- `tool_input.command` is the shell command\n- `model` is always present (this distinguishes Codex from Claude)\n- `cwd` is the working directory\n\n### Claude Code (`SessionStart`)\n\nFires when a new Claude Code session begins. **Not logged** — used only for detection.\n\n```json\n{\n  \"event\": \"SessionStart\",\n  \"model\": \"claude-opus-4-6\"\n}\n```\n\nThe `model` field is only available in this event, not in `PostToolUse`. See [Design decisions](#design-decisions) for why this is not cached.\n\n### Claude Code (`PostToolUse`, matcher: `Bash`)\n\nFires after a Bash tool call. Passive — no stdout response required.\n\n```json\n{\n  \"tool_input\": {\n    \"command\": \"git status\"\n  },\n  \"cwd\": \"/Users/you/project\"\n}\n```\n\n- Same shape as Codex but **without a `model` field** — this is how `classify` distinguishes them\n- Model is logged as `\"unknown\"` for Claude commands\n\n## Log format\n\nEach entry is a single line with pipe-delimited fields:\n\n```\nYYYY-MM-DD HH:MM:SS | source:model | cwd | command\n```\n\n### Examples\n\n```\n2026-04-07 14:22:44 | cursor:gpt-5.3 | /tmp | echo cursor-test\n2026-04-07 14:22:44 | chatgpt:gpt-4 | /Users/you/proj | go test ./...\n2026-04-07 14:22:44 | claude:unknown | /Users/you/media | git status\n2026-04-07 14:22:44 | chatgpt:gpt-4 | /tmp | echo foo\\necho bar\n```\n\n- Newlines in commands are escaped as `\\n` and `\\r` to keep one line per entry\n- Log files are partitioned by month: `logs/2026-04.log`, `logs/2026-05.log`, etc.\n\n### Error entries\n\nScript errors are logged to the same file with the format:\n\n```\nYYYY-MM-DD HH:MM:SS | ERROR | context | message | stdin: \u003cfirst 200 chars\u003e\n```\n\nExample:\n\n```\n2026-04-07 14:24:34 | ERROR | parse | Unexpected token 'o' ... | stdin: not valid json{{{\n```\n\nThe `context` field indicates where the error occurred: `parse` (JSON parsing), `write` (log file I/O), or `main` (top-level).\n\n## Hook configuration reference\n\n### Claude Code — `~/.claude/settings.json`\n\n```json\n{\n  \"hooks\": {\n    \"SessionStart\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"/usr/local/bin/node \u003cproject\u003e/dist/index.js \u003cproject\u003e/logs\"\n          }\n        ]\n      }\n    ],\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Bash\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"/usr/local/bin/node \u003cproject\u003e/dist/index.js \u003cproject\u003e/logs\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nTwo hooks are registered: `SessionStart` (detected but not logged) and `PostToolUse` with the `Bash` matcher (logged).\n\n### ChatGPT / Codex — `~/.codex/hooks.json`\n\n```json\n{\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Bash\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"/usr/local/bin/node \u003cproject\u003e/dist/index.js \u003cproject\u003e/logs\",\n            \"statusMessage\": \"Logging terminal command\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nCodex also requires `~/.codex/config.toml` to have hooks enabled and the log directory in the sandbox write list:\n\n```toml\n[features]\ncodex_hooks = true\n\n[sandbox_workspace_write]\nwritable_roots = [\n  \"\u003cproject\u003e/logs\"\n]\n```\n\n### Cursor — `~/.cursor/hooks.json`\n\n```json\n{\n  \"version\": 1,\n  \"hooks\": {\n    \"afterShellExecution\": [\n      {\n        \"command\": \"/usr/local/bin/node \u003cproject\u003e/dist/index.js \u003cproject\u003e/logs\"\n      }\n    ]\n  }\n}\n```\n\n## Install script\n\n`src/install.ts` idempotently registers hooks in all three tools. Run with:\n\n```bash\nnpm run install-hooks\n```\n\nFor each provider it:\n- Reads the existing config file (or starts from `{}` if missing)\n- Finds any existing `cmd-log` hook entry by checking if the command string contains `\"cmd-log\"`\n- Updates the entry in place, or appends a new one if not found\n- Writes the file back\n\nFor Codex it also ensures `config.toml` has `codex_hooks = true` and the log directory in `writable_roots`.\n\nThe script derives all paths relative to its own location (`import.meta.dirname`), so it works regardless of where the project is cloned.\n\n## CLI usage\n\n```bash\n# The logging script reads JSON from stdin and requires a log directory argument:\necho '{\"tool_input\":{\"command\":\"echo hi\"},\"model\":\"gpt-4\",\"cwd\":\"/tmp\"}' \\\n  | node dist/index.js ./logs\n\n# Without the argument, it prints usage and exits with code 1:\necho '{}' | node dist/index.js\n# stderr: Usage: cmd-log \u003clog-directory\u003e\n```\n\n## npm scripts\n\n| Script | Description |\n|--------|-------------|\n| `npm run build` | Compile TypeScript to `dist/` |\n| `npm run clean` | Remove `dist/` |\n| `npm run rebuild` | Clean + build |\n| `npm run install-hooks` | Register hooks in Claude, Codex, and Cursor configs |\n\n## Design decisions\n\n### Claude model is logged as \"unknown\"\n\nClaude Code's `PostToolUse` hook payload does not include the model name. The `SessionStart` event does, but caching it to a file (the v1 approach) breaks when multiple Claude sessions run in parallel — they overwrite each other's cached model. Rather than introduce fragile state, commands from Claude are logged with `model: \"unknown\"`.\n\nIf Anthropic adds a session identifier or model field to the `PostToolUse` payload in the future, this can be revisited.\n\n### Cursor uses `afterShellExecution`, not `beforeShellExecution`\n\nCursor's `beforeShellExecution` hook is an **active gate** — the script must write `{\"permission\": \"allow\"}` to stdout or the command is blocked. This is inappropriate for passive logging:\n\n- Any script error would block all Cursor shell commands\n- The hook is designed for approval workflows, not observation\n\n`afterShellExecution` is a passive, fire-and-forget hook that fires after the command runs. No stdout response is needed. Note: community reports suggest this hook can be inconsistent in the Cursor CLI (works reliably in the IDE).\n\n### Source detection is payload-based, not config-based\n\nRather than requiring each hook config to pass a `--source=cursor` flag, the script infers the source from the JSON payload structure. This keeps the hook commands identical across providers (same script, same args) and avoids configuration drift.\n\nThe detection relies on each tool having a unique payload fingerprint — see [Source detection](#source-detection).\n\n## Adding a new provider\n\n1. **Define the event type** in `src/index.ts`:\n\n   ```typescript\n   export interface NewToolCommand {\n     kind: \"newtool-command\";\n     command: string;\n     cwd: string;\n     model: string;\n   }\n   ```\n\n2. **Add it to the union types**:\n\n   ```typescript\n   export type HookEvent = CursorCommand | CodexCommand | ClaudeCommand | ClaudeSession | NewToolCommand;\n   export type CommandEvent = CursorCommand | CodexCommand | ClaudeCommand | NewToolCommand;\n   ```\n\n3. **Add a source label**:\n\n   ```typescript\n   const SOURCE_LABEL: Record\u003cCommandEvent[\"kind\"], string\u003e = {\n     // ...existing entries\n     \"newtool-command\": \"newtool\",\n   };\n   ```\n\n4. **Add classification logic** in `classify()` — identify the unique payload fingerprint and extract fields.\n\n5. **Add an install function** in `src/install.ts` for the new tool's config file format.\n\n6. **Build and install**:\n\n   ```bash\n   npm run rebuild\n   npm run install-hooks\n   ```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftywalch%2Fllm-cmd-logger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftywalch%2Fllm-cmd-logger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftywalch%2Fllm-cmd-logger/lists"}