feat(dogfooding): Claude Code PreToolUse hook that runs Tegata on self#15
feat(dogfooding): Claude Code PreToolUse hook that runs Tegata on self#15
Conversation
Adds tools/claude-code-hook.mjs, a zero-config PreToolUse hook that classifies every Claude Code tool call (Bash, Edit, Write, Read, MCP servers, subagents, ...) into an ActionType + riskScore and routes it through tegata.propose(). Decisions are appended to ~/.claude/tegata-audit.jsonl. Default shadow mode never blocks — it only records what Tegata *would* have done, so the hook is safe to turn on right away. Flip TEGATA_HOOK_ENFORCE=1 to have denied/escalated decisions block tool calls via exit code 2. The hook imports Tegata from the repo's built dist/ so it exercises the same public API shipped to npm as tegata@preview. All error paths fall through to exit 0 — a broken local build can never derail the host agent. Motivation: an authorization SDK whose author doesn't use it on their own agent carries no weight. Running it on every Claude Code session also seeds real usage data for the v0.2 TegataReporter design and for the first English blog post. docs/dogfooding.md explains the setup, the classification table, and how to query the audit log. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a dogfooding workflow: a Claude Code PreToolUse Node.js hook classifies tool invocations into Tegata actions with risk scores, calls Changes
Sequence DiagramsequenceDiagram
actor CT as Claude Code Tool
participant H as claude-code-hook
participant T as Tegata
participant L as Audit Log
participant E as stderr/exit
CT->>H: Invoke hook (stdin JSON: tool_name, tool_input, session_id, cwd)
H->>H: Parse JSON & classify into Action (type, riskScore)
H->>T: tegata.propose({ proposer: "claude-code", action, params })
T->>T: Evaluate proposal (allowed / denied / escalated, tier, reason)
T-->>H: Return decision object
H->>L: Append JSONL audit record (~/.claude/tegata-audit.jsonl)
alt TEGATA_HOOK_ENFORCE != "1" (SHADOW)
H->>E: exit 0
else TEGATA_HOOK_ENFORCE = "1" (ENFORCE)
alt decision.status == "denied" or "escalated"
H->>E: write decision.reason to stderr
H->>E: exit 2
else
H->>E: exit 0
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b9f6b894b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const c = (cmd ?? "").trim(); | ||
| if (/^git\s+push\b.*(--force|-f\b|--force-with-lease)/.test(c)) | ||
| return { type: "shell:git:push-force", riskScore: 95 }; | ||
| if (/^git\s+push\b/.test(c)) return { type: "shell:git:push", riskScore: 70 }; |
There was a problem hiding this comment.
Raise git push risk above enforcement threshold
In enforce mode, this hook creates Tegata with escalateAbove: 70, but plain git push is classified with riskScore: 70. Tegata escalates only when riskScore > escalateAbove, so a normal push is approved and not blocked, which contradicts the stated intent that pushes should cross the default threshold and weakens enforcement for a high-impact action.
Useful? React with 👍 / 👎.
| /^git\s+(commit|add|checkout|merge|rebase|stash|tag|fetch|pull)\b/.test(c) | ||
| ) | ||
| return { type: "shell:git:write", riskScore: 40 }; | ||
| if (/\brm\s+-rf?\b/.test(c)) |
There was a problem hiding this comment.
Match destructive rm flag permutations
The recursive-delete detector only matches rm -r/rm -rf (/\brm\s+-rf?\b/), so equivalent invocations like rm -fr or rm -r -f are misclassified as generic shell commands. In enforce mode those variants stay below escalation risk and can execute without the intended high-risk gate.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request introduces dogfooding support for Tegata by integrating it with Claude Code via a PreToolUse hook. It includes a new documentation guide, a hook script (tools/claude-code-hook.mjs) that classifies and evaluates tool calls, and updates to the README. Feedback was provided to improve the robustness of the regex used for identifying destructive shell commands to avoid false positives and better handle various flag combinations.
| /^git\s+(commit|add|checkout|merge|rebase|stash|tag|fetch|pull)\b/.test(c) | ||
| ) | ||
| return { type: "shell:git:write", riskScore: 40 }; | ||
| if (/\brm\s+-rf?\b/.test(c)) |
There was a problem hiding this comment.
The regex for rm -rf is not anchored to the start of the command, which can lead to false positives if the string appears in arguments (e.g., git commit -m "rm -rf"). Additionally, it is quite restrictive regarding flag order and combinations (e.g., it won't match rm -fr or rm -rfv).
| if (/\brm\s+-rf?\b/.test(c)) | |
| if (/^rm\s+-[a-z]*r[a-z]*f?\b/.test(c)) |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tools/claude-code-hook.mjs`:
- Around line 178-190: The audit JSON currently omits decision.reason and other
decision metadata; update the appendFileSync payload to include decision.reason
(and any other relevant fields from the decision object) alongside
decision.status and decision.tier so shadow-mode logs explain why calls were
denied/escalated; locate the appendFileSync call that writes to AUDIT_PATH (uses
sessionId, cwd, toolName, type, riskScore, decision.status, decision.tier,
SHADOW_MODE) and add decision.reason (and any decision.* fields needed) into the
object before JSON.stringify.
- Around line 149-155: The import of the local bundle uses the POSIX path string
in distEntry which fails on Windows; change the import to use a file:// URL by
converting distEntry to a file URL (e.g., via pathToFileURL or new URL) before
calling await import(...). Update the code around the variables here and
distEntry where ({ Tegata } = await import(distEntry)) is attempted so it
imports the file URL instead of the raw absolute path and preserve the existing
try/catch.
- Around line 203-210: Replace the async stderr write + immediate exit with a
synchronous write that includes the decision.reason so the block message is not
lost: instead of calling process.stderr.write(...) then process.exit(2) in the
branch that checks decision.status === "denied" || decision.status ===
"escalated", build a single string containing toolName, type, riskScore,
decision.status and decision.reason and write it synchronously with
fs.writeSync(process.stderr.fd, message) before calling process.exit(2); update
the code around the decision/status handling to use fs.writeSync and include the
decision.reason field.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7ae3abd0-ecf4-4129-a8ee-0a9c87677669
📒 Files selected for processing (3)
README.mddocs/dogfooding.mdtools/claude-code-hook.mjs
- Raise `git push` riskScore 70→71 so it actually crosses the default `escalateAbove: 70` (which uses strict `>`) in enforce mode. - Harden `rm -rf` detector: anchor to start of command, match flag permutations (`-fr`, `-rfv`, `-r -f`, `--recursive`), and handle `sudo`. Avoids false positives like `git commit -m "rm -rf ..."`. - Windows: convert `distEntry` to a `file://` URL via `pathToFileURL()` before `await import()`. Absolute filesystem paths like `C:\...` are rejected by Node's ESM loader. - Audit log now includes `decision.reason`, `proposal_id`, and `decision_ts` — shadow-mode data can explain *why* a call was denied or escalated. - Enforce-mode stderr write: swap `process.stderr.write()` for `fs.writeSync(process.stderr.fd, ...)` so the block message is fully flushed before `process.exit(2)`. Also includes `decision.reason`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clarify safeExit() fail-open semantics: internal errors (bad stdin, missing dist) silently allow the tool call even in enforce mode. Tegata's own denied/escalated verdicts still block, via a separate path. Drops the unused `code` parameter. - docs/dogfooding.md: note ~100–300 ms per-call hook overhead and the unbounded growth of ~/.claude/tegata-audit.jsonl with rotation hints. Addresses review comments M2 / L1 / L4 on PR #15. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
tools/claude-code-hook.mjs— a zero-configPreToolUsehook that classifies every Claude Code tool call into anActionType+riskScoreand routes it throughtegata.propose(). Decisions stream into~/.claude/tegata-audit.jsonl.TEGATA_HOOK_ENFORCE=1to have denied/escalated decisions block tool calls with exit 2.Tegatafrom the repo's builtdist/— exercises the same public API shipped to npm astegata@preview. All error paths fall through toexit 0; a broken local build can never derail the host agent.docs/dogfooding.mddocuments the setup, the classification table (Read=5, Edit=40, git push=70, git push --force=95, rm -rf=85, MCP read=10, MCP write=40, ...), and how to query the audit log.Motivation
An authorization SDK whose author doesn't use it on their own agent carries no weight. Running Tegata on every local Claude Code session also seeds real usage data for the v0.2
TegataReporterdesign, catches API pain before v0.1.0 GA freezes it, and provides concrete numbers for the first English blog post ("The Missing Layer in Multi-Agent Systems").Verification
pnpm run buildproduces a workingdist/pnpm run typecheckgreenpnpm run lintgreen (prettier + eslint).claude/settings.local.json(gitignored) and firing on every tool call of this very PR's authoring session — confirmed by fresh entries in~/.claude/tegata-audit.jsonlwith the currentsession_idNext steps (out of scope for this PR)
TEGATA_HOOK_ENFORCE=1once classification is stable🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation