Skip to content
Open
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: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,17 @@ import (
"fmt"

"github.com/chaoss/disclosure/detection"
"github.com/chaoss/disclosure/detection/coauthor"
"github.com/chaoss/disclosure/detection/committer"
"github.com/chaoss/disclosure/detection/message"
"github.com/chaoss/disclosure/detection/trailer"
"github.com/chaoss/disclosure/detection/toolmention"
"github.com/chaoss/disclosure/scan"
)

func main() {
detectors := []detection.Detector{
&committer.Detector{},
&coauthor.Detector{},
&message.Detector{},
&toolmention.Detector{},
&trailer.Detector{},
}

report, err := scan.ScanCommitRange("/path/to/repo", "base..head", detectors)
Expand Down Expand Up @@ -169,10 +167,9 @@ go test ./...
```
detection/ Core types: Detector interface, Finding, Confidence, Input
detection/committer/ Known AI bot committer emails
detection/coauthor/ Co-Authored-By trailer parsing
detection/gitnotes/ git-ai authorship logs from refs/notes/ai
detection/message/ Commit message pattern matching
detection/toolmention/ AI tool name mentions in text
detection/trailer/ Look for known trailers in commit message
gitops/ go-git wrapper for reading commits
scan/ Orchestration: run detectors over commits or text
output/ JSON and human-readable text formatters
Expand Down
10 changes: 4 additions & 6 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import (
"strings"

"github.com/chaoss/disclosure/detection"
"github.com/chaoss/disclosure/detection/coauthor"
"github.com/chaoss/disclosure/detection/committer"
"github.com/chaoss/disclosure/detection/gitnotes"
"github.com/chaoss/disclosure/detection/message"
"github.com/chaoss/disclosure/detection/toolmention"
"github.com/chaoss/disclosure/detection/trailer"
"github.com/chaoss/disclosure/output"
"github.com/chaoss/disclosure/scan"
"github.com/spf13/cobra"
Expand All @@ -33,18 +32,17 @@ const (
func allDetectors() []detection.Detector {
return []detection.Detector{
&committer.Detector{},
&coauthor.Detector{},
&gitnotes.Detector{},
&message.Detector{},
&trailer.Detector{},
&toolmention.Detector{},
}
}

// Run is the main entry point for the CLI. Returns an exit code.
func Run(args []string, stdout, stderr io.Writer) int {
rootCmd := &cobra.Command{
Use: "disclosure",
Short: "Detect AI-generated contributions",
Use: "disclosure",
Short: "Detect AI-generated contributions",
Long: `Disclosure is a standalone CLI tool that detects AI-generated contributions
in git repositories. It works entirely from git-level data (commit emails,
messages, trailers) with no platform API dependencies.
Expand Down
2 changes: 1 addition & 1 deletion cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func TestFilterReport(t *testing.T) {
Hash: "abc123",
Findings: []detection.Finding{
{Detector: "toolmention", Tool: "Claude", Confidence: 1, Detail: "text"},
{Detector: "coauthor", Tool: "Claude Code", Confidence: 3, Detail: "trailer"},
{Detector: "trailer", Tool: "Claude Code", Confidence: 3, Detail: "trailer"},
},
},
},
Expand Down
46 changes: 0 additions & 46 deletions detection/coauthor/coauthor.go

This file was deleted.

108 changes: 0 additions & 108 deletions detection/coauthor/coauthor_test.go

This file was deleted.

28 changes: 4 additions & 24 deletions detection/committer/committer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,13 @@ import (
"github.com/chaoss/disclosure/detection"
)

// knownAgentCommitters maps GitHub noreply emails to AI tool names.
var knownAgentCommitters = map[string]string{
"209825114+claude[bot]@users.noreply.github.com": "Claude",
"215619710+anthropic-claude[bot]@users.noreply.github.com": "Claude (Anthropic)",
"208546643+claude-code-action[bot]@users.noreply.github.com": "Claude Code Action",
"198982749+copilot@users.noreply.github.com": "GitHub Copilot (agent)",
"167198135+copilot[bot]@users.noreply.github.com": "GitHub Copilot (chat)",
"206951365+cursor[bot]@users.noreply.github.com": "Cursor",
"215057067+openai-codex[bot]@users.noreply.github.com": "OpenAI Codex",
"199175422+chatgpt-codex-connector[bot]@users.noreply.github.com": "Codex via ChatGPT",
"176961590+gemini-code-assist[bot]@users.noreply.github.com": "Gemini Code Assist",
"208079219+amazon-q-developer[bot]@users.noreply.github.com": "Amazon Q Developer",
"158243242+devin-ai-integration[bot]@users.noreply.github.com": "Devin",
"205137888+cline[bot]@users.noreply.github.com": "Cline",
"230936708+continue[bot]@users.noreply.github.com": "Continue.dev",
"201248094+sourcegraph-cody[bot]@users.noreply.github.com": "Sourcegraph Cody",
"220155983+jetbrains-ai[bot]@users.noreply.github.com": "JetBrains AI",
"136622811+coderabbitai[bot]@users.noreply.github.com": "CodeRabbit",
}

// numericPrefixIndex maps the numeric prefix from GitHub noreply emails to tool names.
// This handles issue #4: when a bot's username changes, the numeric ID stays the same.
var numericPrefixIndex map[string]string

func init() {
numericPrefixIndex = make(map[string]string, len(knownAgentCommitters))
for email, name := range knownAgentCommitters {
numericPrefixIndex = make(map[string]string, len(detection.KnownAgentCommitters))
for email, name := range detection.KnownAgentCommitters {
if idx := strings.Index(email, "+"); idx > 0 {
numericPrefixIndex[email[:idx]] = name
}
Expand All @@ -51,7 +31,7 @@ func (d *Detector) Detect(input detection.Input) []detection.Finding {
}

// Direct match against known emails
if name, ok := knownAgentCommitters[email]; ok {
if name, ok := detection.KnownAgentCommitters[email]; ok {
return []detection.Finding{{
Detector: d.Name(),
Tool: name,
Expand All @@ -62,7 +42,7 @@ func (d *Detector) Detect(input detection.Input) []detection.Finding {

// Numeric prefix match for GitHub noreply emails (#4).
// Format: <numeric-id>+<username>@users.noreply.github.com
if strings.HasSuffix(email, "@users.noreply.github.com") {
if strings.HasSuffix(email, detection.GithubNoReplyEmailSuffix) {
if idx := strings.Index(email, "+"); idx > 0 {
prefix := email[:idx]
if name, ok := numericPrefixIndex[prefix]; ok {
Expand Down
2 changes: 1 addition & 1 deletion detection/committer/committer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func TestDetectAllKnownEmails(t *testing.T) {
d := &Detector{}
for email, expectedName := range knownAgentCommitters {
for email, expectedName := range detection.KnownAgentCommitters {
input := detection.Input{CommitEmail: email}
findings := d.Detect(input)
if len(findings) != 1 {
Expand Down
90 changes: 90 additions & 0 deletions detection/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package detection

import "regexp"

// KnownAgentCommitters maps GitHub noreply emails to AI tool names.
var KnownAgentCommitters = map[string]string{
"209825114+claude[bot]@users.noreply.github.com": "Claude",
"215619710+anthropic-claude[bot]@users.noreply.github.com": "Claude (Anthropic)",
"208546643+claude-code-action[bot]@users.noreply.github.com": "Claude Code Action",
"198982749+copilot@users.noreply.github.com": "GitHub Copilot (agent)",
"167198135+copilot[bot]@users.noreply.github.com": "GitHub Copilot (chat)",
"206951365+cursor[bot]@users.noreply.github.com": "Cursor",
"215057067+openai-codex[bot]@users.noreply.github.com": "OpenAI Codex",
"199175422+chatgpt-codex-connector[bot]@users.noreply.github.com": "Codex via ChatGPT",
"176961590+gemini-code-assist[bot]@users.noreply.github.com": "Gemini Code Assist",
"208079219+amazon-q-developer[bot]@users.noreply.github.com": "Amazon Q Developer",
"158243242+devin-ai-integration[bot]@users.noreply.github.com": "Devin",
"205137888+cline[bot]@users.noreply.github.com": "Cline",
"230936708+continue[bot]@users.noreply.github.com": "Continue.dev",
"201248094+sourcegraph-cody[bot]@users.noreply.github.com": "Sourcegraph Cody",
"220155983+jetbrains-ai[bot]@users.noreply.github.com": "JetBrains AI",
"136622811+coderabbitai[bot]@users.noreply.github.com": "CodeRabbit",
}

// GithubNoReplyEmailSuffix GitHub noreply emails suffix to check committer emails.
var GithubNoReplyEmailSuffix = "@users.noreply.github.com"

// SupportedToolsInMentions List of all supported tools to detect in tool mentions within commits.
var SupportedToolsInMentions = []string{
"Claude Code",
"Claude",
"GitHub Copilot",
"Copilot",
"Cursor",
"Aider",
"OpenAI Codex",
"Codex",
"Gemini Code Assist",
"Amazon Q Developer",
"Amazon Q",
"Devin",
"Cline",
"Continue.dev",
"Sourcegraph Cody",
"Cody",
"JetBrains AI",
"CodeRabbit",
"ChatGPT",
"t3.chat",
"GPT-4",
"Windsurf",
}

// KnownCoAuthorEmails Known emails present with Co-Authored-By trailers
var KnownCoAuthorEmails = map[string]string{
"noreply@anthropic.com": "Claude Code",
"cursoragent@cursor.com": "Cursor",
"noreply@aider.chat": "Aider",
"copilot@github.com": "Copilot",
}

// CoAuthorPattern Regex to look for Co-Authroed-By trailer with email
var CoAuthorPattern = regexp.MustCompile(`(?im)^co-authored-by:\s*[^<]*<([^>]+)>`)

// AssistedByPattern Regex to look for Assisted-By trailer with tool name
var AssistedByPattern = regexp.MustCompile(`(?im)^assisted-by\s*:\s*([^\r\n]+?)\s*$`)

// AiderCommitPrefix The commit prefix added by Aider
var AiderCommitPrefix = "aider:"

// ClaudeAttributionText
var ClaudeAttributionText = "Generated with Claude Code"

// EntireIOTrailers List of EntireIO related trailers to check for
var EntireIOTrailers = []string{
"Entire-Metadata",
"Entire-Metadata-Task",
"Entire-Strategy",
"Entire-Session",
"Entire-Condensation",
"Entire-Source-Ref",
"Entire-Checkpoint",
"Entire-Agent",
}

// ReplitAttributionRegex Regex to detect use of Replit Agent or Assistant
var ReplitAttributionRegex = `(?m)^Replit-Commit-Author:\s*(Agent|Assistant)(?:\r?\nReplit-Commit-Session-Id:\s*([a-fA-F0-9-]+))?(?:\r?\n|$)`

// GitNotesAuthorshipPrefix Authorship prefix to check as a precondition in git notes
var GitNotesAuthorshipPrefix = "authorship/"
Loading
Loading