Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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