Back to 0diff
0diff

Deteccion de agentes de IA en tu codebase

Como 0diff detecta agentes de IA que modifican tu codebase usando una jerarquia de 3 niveles: metadatos de commit, variables de entorno y heuristicas de TTY.

Thales & Claude | March 30, 2026 16 min 0diff
EN/ FR/ ES
0diffai-agentsdetectiongitco-authored-byrust

En 2024, la pregunta era "estas usando IA para escribir codigo?" En 2025, se convirtio en "que IA estas usando?" En 2026, la pregunta que realmente importa es: "cual de los cinco agentes de IA que actualmente modifican tu codebase hizo este cambio especifico?"

Somos Juste (CEO, ZeroSuite) y Claude (CTO de IA). Este es el tercer articulo de nuestra serie sobre la construccion de 0diff -- un rastreador de modificaciones de codigo en tiempo real para la era del desarrollo multi-agente. El primer articulo cubrio por que existe 0diff. El segundo cubrio la vigilancia de archivos en tiempo real y el calculo de diff en Rust. Este cubre la funcionalidad que hace a 0diff diferente de cualquier otra herramienta de diff: la deteccion de agentes de IA.


La crisis de atribucion

Aqui hay un commit de un repositorio real de ZeroSuite:

commit a1b2c3d4
Author: Juste Gnimavo <[email protected]>
Date:   2026-02-12T14:23:00+00:00

    Refactor database connection pool settings

    Co-Authored-By: Claude Opus 4 <[email protected]>

El log de git dice que Juste escribio esto. Eso es tecnicamente cierto -- Juste ejecuto la sesion de terminal. Pero Claude realmente escribio el codigo. Y en este caso, el trailer Co-Authored-By hace eso visible. Este es el buen caso. El caso honesto.

Ahora aqui hay un commit diferente de un equipo diferente (no el nuestro, pero uno del que hemos oido hablar):

commit e5f6g7h8
Author: dev-bot <[email protected]>
Date:   2026-02-12T15:47:00+00:00

    fix: update config

Que hizo este cambio? Un script de CI? GitHub Copilot ejecutandose en el editor de un desarrollador? Cursor haciendo una refactorizacion multi-archivo? Devin trabajando autonomamente en una rama de funcionalidad? Nadie sabe. Los metadatos del commit no revelan nada.

Esta es la crisis de atribucion. Git fue disenado para autores humanos. No tiene concepto de "este commit fue escrito por un humano usando una herramienta de IA" versus "este commit fue generado autonomamente por un agente de IA." El campo Author es lo que sea que este en .gitconfig. El mensaje del commit es lo que sea que el committer (o el agente) decidio escribir.

0diff resuelve esto con una jerarquia de deteccion de tres niveles.


Nivel 1: Metadatos del commit (mayor confianza)

La senal mas confiable es el commit en si. Si un agente de IA o un desarrollador que usa un agente de IA sigue convenciones de atribucion, la evidencia esta ahi mismo en el mensaje del commit o en los trailers Co-Authored-By.

Aqui esta el codigo real de deteccion de agents.rs:

rustpub fn detect_from_commit(&self, commit: &CommitInfo) -> Option<String> {
    let message_lower = commit.message.to_lowercase();

    for pattern in &self.patterns {
        let pattern_lower = pattern.to_lowercase();

        if message_lower.contains(&pattern_lower) {
            return Some(pattern.clone());
        }

        for co_author in &commit.co_authors {
            if co_author.to_lowercase().contains(&pattern_lower) {
                return Some(pattern.clone());
            }
        }
    }

    None
}

Esta funcion verifica dos cosas para cada patron configurado (por defecto: Claude, Cursor, Copilot, Windsurf, Devin):

  1. El mensaje del commit. Si un mensaje de commit contiene "Generated by Copilot" o "Claude session 314" o cualquier cosa que coincida con el patron, tenemos una coincidencia.
  2. Los trailers Co-Authored-By. Aqui es donde vive la senal real. Cuando Claude Code crea un commit, anade Co-Authored-By: Claude <[email protected]>. Cuando un desarrollador usa Copilot, algunos flujos de trabajo anaden trailers similares.

La coincidencia sin distincion de mayusculas y minusculas es deliberada. Hemos visto "Co-Authored-By: claude" (minusculas), "Co-authored-by: Claude" (capitalizacion estandar), y "CO-AUTHORED-BY: CLAUDE" (alguien con problemas de bloq mayus). Todos deben coincidir.

Por que importa Co-Authored-By

El trailer de git Co-Authored-By se esta convirtiendo en el estandar de facto para la atribucion de IA. GitHub lo renderiza. GitLab lo renderiza. Cada herramienta importante de programacion con IA que sigue buenas practicas lo usa. Claude Code lo anade automaticamente. Es lo mas cercano que tenemos a una convencion universal para "este humano y esta IA trabajaron juntos en este commit."

Pero es una convencion, no un requisito. Los agentes pueden configurarse para omitirlo. Los desarrolladores pueden eliminarlo con amend. Los pipelines de CI pueden eliminarlo. Por eso los metadatos del commit son el nivel de mayor confianza pero no el unico nivel.

Extraccion de Co-Authored-By desde git

Para detectar estos trailers, 0diff necesita parsearlos del cuerpo del commit. Aqui esta como lo hace git.rs:

rustpub fn recent_commits(
    &self,
    limit: usize,
) -> Result<Vec<CommitInfo>, Box<dyn std::error::Error>> {
    let limit_arg = format!("-{}", limit);
    let output =
        self.run_git(&["log", &limit_arg, "--format=%H%n%an%n%s%n%aI%n%b%n---END---"])?;

    let mut commits = Vec::new();

    for block in output.split("---END---") {
        let block = block.trim();
        if block.is_empty() {
            continue;
        }

        let mut lines = block.lines();
        let hash = lines.next().unwrap_or("").to_string();
        let author = lines.next().unwrap_or("").to_string();
        let message = lines.next().unwrap_or("").to_string();
        let date = lines.next().unwrap_or("").to_string();

        // Remaining lines are the body -- extract Co-Authored-By
        let body: String = lines.collect::<Vec<_>>().join("\n");
        let co_authors = body
            .lines()
            .filter_map(|l| {
                let trimmed = l.trim();
                if let Some(rest) = trimmed.strip_prefix("Co-Authored-By:") {
                    Some(rest.trim().to_string())
                } else if let Some(rest) = trimmed.strip_prefix("Co-authored-by:") {
                    Some(rest.trim().to_string())
                } else {
                    None
                }
            })
            .collect();

        if !hash.is_empty() {
            commits.push(CommitInfo {
                hash,
                author,
                message,
                date,
                co_authors,
            });
        }
    }

    Ok(commits)
}

Algunas decisiones de diseno que vale la pena explicar:

Cadena de formato personalizada, no libgit2. Usamos git log --format=%H%n%an%n%s%n%aI%n%b%n---END--- para obtener exactamente los campos que necesitamos, separados por saltos de linea, con un marcador centinela entre commits. Esto es mas rapido y simple que enlazar contra libgit2, y funciona en todas partes donde git esta instalado -- que es en todas partes donde 0diff seria util.

Ambas variantes de capitalizacion. El encabezado Co-Authored-By: no tiene capitalizacion canonica. Git mismo usa "Co-authored-by:" en su documentacion. GitHub usa "Co-authored-by:" en su interfaz. Algunas herramientas producen "Co-Authored-By:". Manejamos ambas. Un enfoque mas robusto seria coincidencia de prefijo completamente sin distincion de mayusculas, pero en la practica estas dos variantes cubren cada caso que hemos encontrado.

El cuerpo viene despues del asunto. En la cadena --format, %s da el asunto (primera linea) y %b da el cuerpo (todo lo demas). Los trailers Co-Authored-By viven en el cuerpo, tipicamente al final. Escaneamos cada linea del cuerpo porque algunos commits tienen multiples parrafos antes de los trailers.


Nivel 2: Variables de entorno (confianza media)

Cuando no hay commit que inspeccionar -- por ejemplo, durante la vigilancia de archivos en tiempo real entre commits -- 0diff recurre a la deteccion por variables de entorno:

rustpub fn detect_from_environment(&self) -> Option<String> {
    let checks: &[(&str, &str)] = &[
        ("CLAUDE_CODE", "Claude"),
        ("CURSOR_SESSION", "Cursor"),
        ("GITHUB_COPILOT", "Copilot"),
        ("WINDSURF_SESSION", "Windsurf"),
        ("DEVIN_SESSION", "Devin"),
    ];

    for (var, name) in checks {
        if std::env::var(var).is_ok() {
            return Some(name.to_string());
        }
    }

    None
}

Cada herramienta importante de programacion con IA establece variables de entorno caracteristicas cuando se ejecuta. Claude Code establece CLAUDE_CODE. Cursor establece variables de entorno relacionadas con la sesion. La presencia de estas variables nos dice que un proceso de agente de IA esta activo en el entorno actual.

Este nivel es de confianza media porque:

  1. Detecta la presencia del agente, no su autoria. Si Claude Code esta ejecutandose en una terminal y el desarrollador edita manualmente un archivo en otra ventana, la variable de entorno sigue establecida. 0diff etiquetaria esa edicion manual como una edicion de Claude.
  2. Las variables de entorno pueden falsificarse. Cualquiera puede ejecutar export CLAUDE_CODE=1 y 0diff reportaria a Claude como el agente activo.
  3. Multiples agentes pueden estar activos simultaneamente. Si tanto Claude Code como Cursor estan ejecutandose, 0diff reporta la primera coincidencia en el orden de verificacion.

A pesar de estas limitaciones, la deteccion por entorno llena una brecha importante. Durante 0diff watch, la mayoria de las modificaciones de archivos ocurren entre commits, cuando no hay metadatos de commit que inspeccionar. Las variables de entorno nos dan la mejor senal disponible sobre que herramienta esta haciendo cambios ahora mismo.


Nivel 3: Heuristica de TTY (menor confianza)

El nivel final es una heuristica simple pero sorprendentemente util:

rustpub fn detect_from_tty() -> bool {
    std::io::stdin().is_terminal()
}

Si stdin no es una terminal, el proceso se esta ejecutando en un contexto no interactivo -- un pipeline de CI, un cron job, un script, o un agente de IA operando en modo headless. Esto no nos dice cual agente es responsable, pero nos dice que los cambios probablemente no provienen de un humano tecleando en un teclado.

Cuando esta heuristica se activa (stdin no es una terminal) y ni el Nivel 1 ni el Nivel 2 produjeron una coincidencia, 0diff etiqueta la entrada como "unknown-agent". Esto es un etiquetado honesto: sabemos que probablemente no es un humano, pero no podemos identificar la herramienta especifica.

La verificacion de TTY es el nivel menos preciso, pero captura una clase de modificaciones que de otra manera serian invisibles: scripts automatizados, herramientas internas personalizadas y agentes de IA que no establecen variables de entorno ni dejan metadatos de commit.


La cascada: tag_for_entry

Los tres niveles se combinan en una sola funcion que produce la etiqueta final de agente para cada cambio rastreado:

rustpub fn tag_for_entry(&self, commit: Option<&CommitInfo>) -> Option<String> {
    if let Some(c) = commit {
        if let Some(agent) = self.detect_from_commit(c) {
            return Some(agent);
        }
    }

    if let Some(agent) = self.detect_from_environment() {
        return Some(agent);
    }

    if !Self::detect_from_tty() {
        return Some("unknown-agent".to_string());
    }

    None
}

La logica es una cascada estricta:

  1. Si tenemos un commit y contiene metadatos de agente, usar eso. Esta es la senal de mayor confianza.
  2. Si no, verificar variables de entorno. Esto cubre la vigilancia en tiempo real entre commits.
  3. Si no, verificar si estamos en un contexto no interactivo. Si es asi, etiquetar como unknown-agent.
  4. Si nada de lo anterior coincide, devolver None -- esto es probablemente un humano haciendo cambios interactivamente.

El parametro Option<&CommitInfo> es importante. Durante 0diff watch, el observador obtiene el commit mas reciente para verificar metadatos de agente. Pero el commit mas reciente podria no estar relacionado con el cambio de archivo actual -- el desarrollador podria estar editando archivos sin hacer commit. La cascada maneja esto graciosamente: si la verificacion del commit no produce una coincidencia, todavia tenemos dos niveles mas para probar.


Patrones configurables

Los patrones predeterminados cubren las cinco principales herramientas de programacion con IA de 2026:

toml[agents]
detect_patterns = ["Claude", "Cursor", "Copilot", "Windsurf", "Devin"]
tag_non_human = true

Pero los equipos usan herramientas internas, bots personalizados y sistemas de IA propietarios. El array detect_patterns es completamente configurable:

toml[agents]
detect_patterns = ["Claude", "Cursor", "Copilot", "Windsurf", "Devin", "InternalBot", "CodeGenPipeline"]
tag_non_human = true

Cualquier cadena en este array se compara (sin distincion de mayusculas) contra mensajes de commit, trailers Co-Authored-By y variables de entorno. Anade el nombre de tu bot interno, y 0diff lo detectara.

El flag tag_non_human = true controla si se aplica la heuristica de TTY (Nivel 3). Establecelo a false si solo quieres deteccion de agentes de alta confianza a partir de metadatos de commit y variables de entorno.


Por que git basado en shell fue la decision correcta

Una pregunta que surge en cada revision de codigo de git.rs: por que llamar al shell de git en lugar de usar libgit2 via la crate git2?

Todo el modulo de integracion con git es de 161 lineas. Usa std::process::Command para ejecutar comandos de git:

rustfn run_git(&self, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
    let output = Command::new("git")
        .args(args)
        .current_dir(&self.root)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("git {} failed: {}", args.join(" "), stderr.trim()).into());
    }

    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

Tres razones por las que esto es mejor que libgit2 para nuestro caso de uso:

1. Tamano del binario. La crate git2 trae libgit2-sys, que incluye una biblioteca C, una dependencia de OpenSSL y aproximadamente 3MB de codigo compilado. El binario de release de 0diff es 2.0MB en total. Usar git2 mas que duplicaria eso.

2. Paridad de funcionalidad. libgit2 es una reimplementacion de git, y no soporta cada funcionalidad de git. Las cadenas de formato personalizadas en git log, salida porcelain de blame y algunas opciones de configuracion no estan disponibles. Al llamar al shell del binario real de git, obtenemos 100% de compatibilidad con la version de git que el usuario tenga instalada.

3. Simplicidad. El helper run_git es de 10 lineas. Cada operacion de git en 0diff es una sola llamada de funcion con argumentos de cadena. No hay problemas de lifetime con manejadores de repositorio, no hay APIs basadas en callbacks para recorrer el historial de commits, no hay gestion manual de memoria para objetos de git. La compensacion es la sobrecarga de spawn de proceso, pero para nuestra carga de trabajo (unas pocas llamadas de git por evento de cambio de archivo, con debouncing de 500ms), es insignificante.

La unica desventaja es que 0diff requiere que git este instalado. Como 0diff es una herramienta para desarrolladores que trabajan con repositorios git, esta no es una restriccion significativa.


Consulta por agente: 0diff log --agent

La deteccion de agentes es util durante la vigilancia en tiempo real, pero se vuelve poderosa durante el analisis retrospectivo. El comando 0diff log soporta filtrado por agente:

0diff log --agent "Claude" -n 10

Esto consulta el almacen de historial JSON-lines y devuelve solo entradas donde el campo agent coincide con "Claude" (sin distincion de mayusculas). Combinado con el filtrado por autor, puedes responder preguntas como:

  • "Que cambio Claude en la ultima hora?" -- 0diff log --agent "Claude" -n 50
  • "Que cambios fueron hechos por algun agente de IA?" -- 0diff log --agent "unknown-agent" -n 20 (captura las entradas detectadas por TTY)
  • "Que cambio Juste manualmente (sin agente)?" -- esto requiere verificar entradas donde agent es nulo, lo cual la API de consulta actual no soporta directamente pero es trivial de anadir

La coincidencia de subcadena sin distincion de mayusculas fue una eleccion deliberada. Puedes filtrar con --agent "claude" o --agent "Claude" o incluso --agent "clau" y obtener los mismos resultados. En una herramienta disenada para consultas rapidas en terminal, la flexibilidad importa mas que la precision.


Pruebas de la logica de deteccion

Cuatro tests cubren los escenarios principales de deteccion en agents.rs:

Test 1: Deteccion de co-autor. Un commit con Co-Authored-By: Claude <[email protected]> en sus trailers deberia ser detectado como una modificacion de Claude.

Test 2: Deteccion por mensaje. Un commit con "Generated by Copilot" en el mensaje deberia ser detectado como una modificacion de Copilot.

Test 3: Sin agente. Un commit con un mensaje escrito por humano y sin trailers Co-Authored-By deberia devolver None.

Test 4: Patrones personalizados. Un detector configurado con ["MyBot", "CustomAgent"] deberia detectar "Changes from MyBot session" como una modificacion de MyBot.

rust#[test]
fn test_detect_from_commit_co_author() {
    let detector = default_detector();
    let commit = CommitInfo {
        hash: "abc123".to_string(),
        author: "Juste".to_string(),
        message: "Fix bug".to_string(),
        date: "2026-02-14T10:00:00+00:00".to_string(),
        co_authors: vec!["Claude <[email protected]>".to_string()],
    };

    let result = detector.detect_from_commit(&commit);
    assert_eq!(result, Some("Claude".to_string()));
}

Estos tests son intencionalmente simples y enfocados. Cada uno prueba una sola ruta de deteccion. No hay mocking de git ni del sistema de archivos -- la estructura CommitInfo es datos planos, asi que la construimos directamente.


Lo que la deteccion de agentes no hace

La honestidad sobre las limitaciones es tan importante como explicar las capacidades.

0diff no rastrea pulsaciones de teclas. No sabe si un humano escribio el codigo caracter por caracter o pego la salida de una sesion de chat de IA. Si los metadatos del commit no mencionan un agente y no hay variable de entorno establecida, 0diff no tiene forma de saberlo.

0diff no identifica codigo generado por IA mediante huella digital. Hay proyectos academicos que intentan detectar codigo escrito por IA analizando estilo, patrones y propiedades estadisticas. 0diff no hace esto. La deteccion estilometrica no es confiable, especialmente a medida que los modelos de IA mejoran. Nos basamos en senales explicitas -- metadatos y entorno -- no en heuristicas sobre la calidad del codigo.

0diff no impone la atribucion. Detecta y registra. No bloquea commits que carecen de atribucion de agente. No modifica mensajes de commit. No anade trailers Co-Authored-By. Esas son decisiones de politica para que los equipos las tomen. 0diff te da los datos; lo que hagas con ellos depende de ti.

0diff no rastrea conversaciones de agentes. No registra los prompts dados a Claude ni las sugerencias hechas por Copilot. Rastrea el resultado -- la modificacion del archivo -- no el proceso que lo produjo.


Por que esto importa

En 2024, "quien escribio este codigo?" tenia una respuesta simple. En 2026, no.

Una sesion de desarrollo tipica en ZeroSuite involucra a Juste dando instrucciones a Claude Code a traves de cinco agentes en paralelo, cada uno modificando diferentes partes del codebase simultaneamente. La sesion que construyo 0diff en si misma es un ejemplo perfecto: cinco agentes, tocando ocho archivos fuente, produciendo 2,356 lineas de codigo en 45 minutos. Sin deteccion de agentes, git registraria todo esto como commits de "Juste Gnimavo" -- tecnicamente preciso, profundamente enganoso.

La deteccion de agentes importa por tres razones:

Rendicion de cuentas. Cuando un bug se rastrea hasta un cambio de archivo especifico, saber que agente hizo el cambio te dice que configuracion, que prompt, que ventana de contexto produjo el error. "Claude cambio esto en la sesion 314" es accionable. "Alguien cambio esto" no lo es.

Auditoria. A medida que los agentes de IA se vuelven mas autonomos -- y lo haran -- las organizaciones necesitaran registros de auditoria que distingan decisiones humanas de decisiones de IA. Los marcos regulatorios ya estan emergiendo que requieren esta distincion. 0diff proporciona los datos en bruto.

Aprendizaje. Al rastrear que agentes producen que tipos de cambios, los equipos pueden evaluar el rendimiento de los agentes a lo largo del tiempo. Copilot introduce mas cambios de solo espacios en blanco? Claude produce diffs mas grandes? El modo autonomo de Devin se correlaciona con mas eliminaciones? Estas son preguntas empiricas, y requieren datos.

0diff es esa capa de datos. No juzga. No bloquea. Observa, detecta, registra. El resto depende de ti.


Que sigue

El articulo final de esta serie cubre la historia completa de la construccion: como cinco agentes en paralelo construyeron 0diff en 45 minutos, los siete bugs que corregimos durante la sesion, la preparacion de lanzamiento de 20 minutos tres semanas despues, y como 0diff se compara con alternativas.


Esta es la Parte 3 de la serie "Como construimos 0diff":

  1. Por que construimos un rastreador de cambios de codigo para la era de los agentes de IA
  2. Vigilancia de archivos en tiempo real y calculo de diff en Rust
  3. Deteccion de agentes de IA en tu codebase (estas aqui)
  4. De 5 agentes a produccion: Desplegando 0diff en 20 minutos
Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles