Detection model
How Blamely knows which lines came from an AI and which came from you.
Overview
Detection uses three complementary signal layers, layered from most precise to least:
- PostToolUse hooks — AI tools call
blamely record <tool>directly. Exact file and line range known. Confidence: high. - Editor plugins — the VS Code/Cursor/JetBrains plugin watches the editor command that fires when a Tab completion is accepted. Confidence: high (command-triggered) or medium (heuristic fallback).
- Background watchers — the daemon tails log files and session stores. Confidence: medium or low depending on the source (high for the Devin IDE session databases, which record the exact edit).
Work done entirely off your machine (Devin Cloud) is covered separately, by commit trailers.
At commit time, the attribution join picks the newest edit record covering each added line. Higher-confidence records win ties if they overlap.
PostToolUse hooks (AI tools → daemon)
Each AI tool is configured (by blamely install) to call blamely record <tool> after it writes a file. These are the most precise signals: the hook receives the exact file path and edit content, so Blamely knows the precise lines that changed.
| Tool | Hook triggered by | Gen type |
|---|---|---|
| Claude Code | Every Edit, Write, MultiEdit, NotebookEdit tool call | chat |
| Cursor (Composer/Chat) | Same PostToolUse hook — detected by cursor_version in payload | chat |
| Codex CLI | apply_patch operations in session JSONL | cli |
| Copilot CLI | CLI chat apply events | cli |
| Gemini CLI | File-write operations | cli |
| Devin CLI | write, edit, and apply_patch tool calls; file deletions via exec / shell | cli |
For Claude Code and Cursor, the hook payload also carries the transcript path — Blamely reads the last few turns of the conversation and attaches them to the commit note so you can see what was asked for. Devin CLI’s payload has no transcript, so its edits carry no model, token, or conversation data.
Editor plugins (Tab completion)
Cursor Tab and Copilot inline completions don’t fire a hook. The only way to see them is from inside the editor.
High-confidence path
The plugin listens to the VS Code command bus. When the user accepts an inline completion the editor executes one of:
editor.action.inlineSuggest.commit (Tab — accept full suggestion)
editor.action.inlineSuggest.acceptNextWord (Cmd/Ctrl+Right — accept next word)
editor.action.inlineSuggest.acceptNextLine (accept next line)These commands fire only on completion accepts — not on snippet expansion, code actions, pastes, or LSP quick-fixes. When the plugin sees one of these, it marks the next document change as confidence=high with no heuristics involved.
Medium-confidence fallback
For completions whose provider doesn’t route through those commands (rare), the plugin falls back to a heuristic: a single document change with ≥ 8 non-whitespace characters after trimming is treated as a completion candidate.
Plain Enter keypresses are excluded
Plain Enter keypresses (\n or \n with auto-indent) are explicitly excluded — they don’t pass the non-whitespace threshold. Only insertions with substantial code content qualify.
Paste detection
Before recording a completion, the plugin reads the clipboard. If the inserted text matches the clipboard content (exact, prefix, suffix, or substring match for ≥ 8 chars), the insert is classified as copypaste — a distinct bucket in by_tool that rolls up to the Human side of the AI vs Human bar.
JetBrains plugin
The JetBrains plugin uses AnActionListener and matches action IDs that contain InlineCompletion combined with Insert, Accept, or Apply. GitHub Copilot’s IntelliJ plugin is covered by a separate pattern on com.github.copilot.* accept/insert/apply actions.
Background watchers
The daemon runs these watchers continuously alongside hook ingestion.
CursorLogWatcher
Tails Cursor’s extension-host log files for keywords that indicate a Composer or Agent apply event: composerApply, composer.apply, agentEdit, agent.edit, agent.apply.
Emits tool=cursor, gen_type=chat, confidence=medium with the file path extracted from the log line.
Cursor Tab log files (Cursor Tab.log inside cursor-always-local directories) are explicitly skipped — those log entries fire on suggestion generation, not on acceptance, so using them would over-count completions. Tab completions are exclusively handled by the editor plugin.
CopilotLogWatcher
Tails the GitHub Copilot plugin’s log files across VS Code, Cursor, and all JetBrains IDEs:
- VS Code / Cursor:
~/Library/Application Support/{Code,Cursor}/logs/(macOS) - JetBrains IDEs: all subdirectories under
~/Library/Logs/JetBrains/are discovered dynamically — new IDEs are picked up without code changes.
Two detection strategies run in parallel:
| Strategy | Pattern | Result |
|---|---|---|
| JetBrains fetch log (high precision) | #copilot - [fetchCompletions] … | gen_type=completion with model from URL |
| JetBrains fetch log (high precision) | #copilot - [fetchChat] … | gen_type=chat |
| AutoModelService line | #copilot - [AutoModelService] Fetched auto model for active … : <model> | Updates active model for subsequent fetch lines |
| Generic accept marker | Lines matching copilot + accept/applied/completion/inline | gen_type=completion or chat based on keyword |
Cursor Tab log files are skipped here too — the isCursorTabLog guard prevents mis-attributing Cursor completions as Copilot.
CopilotChatWatcher
Reads Copilot Chat’s per-workspace session JSONL files (workspaceStorage/<hash>/chatSessions/<id>.jsonl). These use a delta-encoding format:
kind=0— initial snapshot, capturesselectedModel.identifierkind=1— model updatekind=2at key pathresponse— Copilot streamed a response; emitstool=copilot, gen_type=chat, model=<selected model>
This gives Blamely the exact model the user had selected in the chat panel (e.g. copilot/gpt-4o, copilot/claude-3-5-sonnet).
CodexWatcher
Tails ~/.codex/sessions/*.jsonl for apply_patch operations. Captures model and token usage from the surrounding response events.
ClaudeDesktopWatcher
The Claude Desktop app’s “cowork” sandbox runs file operations inside a VM with your repo mounted, and — unlike Claude Code — exposes no PostToolUse hook. The only observable signal is the app’s own logs under ~/Library/Logs/Claude:
coworkd.logrecords the VM’s process spawns (thebashcommands that write, move, or delete files).main.logmaps each VM mount back to the host repo path on your machine.
The watcher tails coworkd.log for file-mutating commands and attributes them as tool=claude, gen_type=chat, confidence=medium:
- Deletions are recorded straight from the
rm/git rmtargets, hashed fromHEADcontent — so they attribute correctly even if you commit immediately after. - Writes are reconciled from the working tree a few seconds after the command, once the VM has synced the change back to the host filesystem.
Best-effort and macOS-only
Because the log format is private to Claude Desktop, this watcher is best-effort and macOS-only today; on other platforms (and where those logs don’t exist) it idles. Claude Code’s hook-based detection is unaffected — this covers only the Desktop app’s sandbox.
DevinIDEWatcher
The agent inside the Devin IDE has no hook framework. The IDE keeps one SQLite database per agent session, and the watcher reads completed edit tool calls from them — with the exact path and text written:
- macOS:
~/Library/Application Support/devin/User/acp-messages/*.db - Linux:
~/.config/devin/User/acp-messages/*.db - Windows:
%APPDATA%\devin\User\acp-messages\*.db
Emits tool=devin, gen_type=chat, confidence=high. Each database is opened read-only and closed right away, so it coexists with the running IDE. Cloud sessions recorded in the same database are skipped: their paths point into a remote sandbox and carry no content.
Commit trailers (Devin Cloud)
Every other signal observes an edit as it happens on your machine. A Devin Cloud session edits files in a remote sandbox, so there is nothing local to observe — the work arrives as a finished commit carrying Devin’s own trailer:
Co-Authored-By: Devin <…+devin-ai-integration[bot]@users.noreply.github.com>When a git pull brings in such a commit, the global post-merge hook attributes it and credits its lines to tool=devin, gen_type=chat. The match keys on the devin-ai-integration bot account, not the editable display name.
This is commit-scoped — it can’t tell which lines the agent wrote, so it credits all of them — and it never overrides an edit that was recorded locally. Only Devin’s trailer is read: other tools that write Co-Authored-By trailers are already observed by their hooks.
Gen types
The gen_type field in every line entry shows how the code was produced.
| Gen type | When it’s set | Tools |
|---|---|---|
chat | A chat or Composer session applied an edit | Claude Code, Cursor Composer, Copilot Chat, Gemini chat, Devin IDE, Devin Cloud |
completion | An inline Tab completion was accepted | Cursor Tab, Copilot Tab |
cli | A CLI agent ran and patched files | Codex CLI, Gemini CLI, Copilot CLI, Devin CLI |
human | Human-typed lines, deleted lines, and copy-pasted code of unknown origin | — |
by_gen_type in the git note tallies all four. Deleted lines always count as human — removing code is a human action regardless of who wrote the original.
Confidence levels
| Confidence | Source | Meaning |
|---|---|---|
high | PostToolUse hook; editor plugin inlineSuggest.commit command; DevinIDEWatcher | Exact file + line range known; no guessing |
medium | CursorLogWatcher; CopilotLogWatcher JetBrains fetch lines; editor plugin heuristic | File identified but range is whole-file or approximate |
low | CopilotChatWatcher; CopilotWatcher session marker | Session active, no file path; used to corroborate other signals |
The attribution join is newest-wins per line. A later high row always beats an earlier low row covering the same line.
What is not detected
| Scenario | Why |
|---|---|
| Plain Enter keypresses | Excluded explicitly — only inserts with ≥ 8 non-whitespace chars qualify on the medium-confidence plugin path |
Snippet expansion (typing for<Tab>) | Looks identical to a Tab completion from the editor’s API on the medium-confidence path; may be attributed as completion |
| LSP code actions and auto-imports | Same limitation — the editor API doesn’t distinguish them from completions at the document-change level |
| Edits older than 8 hours before a commit | Outside the lookback window; intentional — older edits drift too far from the assistant action that produced them |
| Cursor Tab via log files | The log fires on suggestion generation, not acceptance; Cursor Tab is tracked only via the editor plugin |
| Whitespace-only inserts | Filtered at attribution time; no meaningful content to attribute |
Next
- Architecture — the end-to-end flow.
- Debugging with log — see exactly what Cursor signals the daemon receives.
- Installation: VS Code / Cursor / JetBrains — set up the editor plugin.
- Excluding files — keep generated files out of attribution.
Something unclear or wrong? Tell us.