| name | secret-output-guard |
| description | Guardrail conductual cross-repo que previene exposición de tokens, secrets y API keys en outputs de agentes. Usar antes de commitear, publicar retros, escribir mailbox, pegar salida de az/gh/kubectl/curl, o generar cualquier output visible. Motivada por incidente AF-1k.16i1c (gate-token leak). |
Skill: secret-output-guard
Purpose
Evitar que tokens, claves API, refresh tokens, contraseñas o credenciales aparezcan en outputs de agentes (chat transcripts, commits, mailbox messages, audit docs, retros).
Why this skill exists
Incidente real 2026-04-26 (sesión umbral-bot-copilot AF-1k.16i1c → i1d): un gate token de Container App se imprimió completo en el transcript local del Copilot Chat. Costó una rotación inmediata + commit explícito de revocación. Plan Q2 §6 lista esto como riesgo activo.
Este skill es la regla operativa cross-agent: cualquier output sustantivo pasa por este checklist antes de quedar persistido.
When to invoke
Antes de cualquier de estos:
- Imprimir output de comando que pueda incluir credenciales (
az, gh, curl con headers, scripts que leen .env, kubectl get secrets, etc.).
- Commitear archivo (incluido
.md, .yaml, .json, .ps1, .py).
- Escribir mensaje en
.agents/mailbox/.
- Cerrar Friday retro.
- Mover páginas o adjuntos en Notion via MCP (un adjunto puede contener un screenshot con tokens).
The checklist (8 patrones a buscar)
- Tokens explícitos:
[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{6,} (JWT-like), ghp_, ghs_, gho_, github_pat_, sk-, sk_test_, sk_live_, Bearer , Basic .
- Azure:
DefaultEndpointsProtocol=, AccountKey=, claves de Cognitive Services / OpenAI / Search (~32–88 chars base64), client_secret=.
- Google:
AIza..., ya29., 1//0, GOOGLE_REFRESH_TOKEN=.
- Notion / Granola:
secret_..., ntn_, integration tokens.
- MercadoPago / Stripe / pasarelas:
APP_USR-...-...-..., whsec_, pi_.
- SSH / GPG:
BEGIN OPENSSH PRIVATE KEY, BEGIN PGP PRIVATE KEY, BEGIN RSA PRIVATE KEY.
- Connection strings:
mongodb+srv://, postgres://user:pass@, mysql://, cualquier URL con :.+@.
- Variables de entorno con nombre sospechoso mostradas con valor: cualquier env que contenga
KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API_KEY en su nombre.
What to do if you find one
| Situación | Acción |
|---|
| Token apareció en chat output (no commiteado) | Pedir a David rotación inmediata. Reemplazar valor por [REDACTED] antes de continuar. NO commitear el output. |
| Token ya está commiteado | Stop. Avisar a David. Rotar primero, después usar git filter-repo o equivalente. NO basta con un commit que borre — el token sigue en historia. |
| Variable de entorno con nombre sospechoso pero sin valor mostrado | OK — solo el nombre es información pública. |
Comando va a imprimir token (ej: az ad sp create-for-rbac) | Redirigir output a archivo en .gitignore, leer con redacción, o usar flag --query para extraer solo lo no-secreto. |
Reglas operativas para agentes
- Default-deny. Si tenés duda de si un valor es sensible, asumí que sí.
- Nunca pegar
.env completo en chat ni en commits, ni siquiera con valores fake.
- Comandos
az/gcloud/kubectl/gh que retornan credenciales: siempre con --query o | ConvertFrom-Json | Select para acotar.
- Si pedís a David un secreto: que lo paste en input efímero (terminal interactivo), no en chat history.
- Workspace settings:
.vscode/settings.json no debe tener terminal.integrated.persistentSessionReviveProcess: never desactivado (los buffers de terminal pueden persistir en disco).
- Friday retro: sección
Quality gate incluye checkbox [ ] No hay tokens/secrets en este doc. No se commitea retro sin esa marca.
- Si un agente externo (Codex, otro hilo Copilot) pasó un secret en handoff via mailbox: rechazar el mensaje con
status: cancelled + razón, pedir reenvío sin secreto.
- Inspección de
/proc/$PID/environ (Linux runtime forensics): NUNCA cat, strings, ni grep con patrones que matcheen el VALOR del secret (ej: grep "COPILOT", grep "TOKEN", grep "sk-"). El environ contiene los secretos en plaintext y cualquier match imprime el valor completo. Incidente motivante F-INC-001 (2026-05-07): durante audit OpenClaw, un agente ejecutó grep "COPILOT" /proc/$PID/environ y volcó COPILOT_GITHUB_TOKEN=ghp_... al output → rotación inmediata. Patrón aprobado — listar solo nombres de var, o nombre + longitud + fingerprint:
tr '\0' '\n' < /proc/$PID/environ | awk -F= '{print $1}' | sort -u
tr '\0' '\n' < /proc/$PID/environ \
| awk -F= '/(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/ {
val=$0; sub(/^[^=]*=/, "", val);
"printf %s \"" val "\" | sha256sum | cut -c1-8" | getline h;
printf "%s=<len=%d sha8=%s>\n", $1, length(val), h
}' | sort
Regla equivalente para cualquier dump de entorno (env, printenv, ps eww, systemctl show --property=Environment): aplicar el mismo filtrado nombre-only o nombre+fingerprint.
Tool-emitted partial leaks
Algunas herramientas oficiales imprimen prefijos parcialmente enmascarados de secretos por defecto. Aunque "solo" sea el prefijo (ej: gho_abcd******), es suficiente para correlacionar tokens en audit logs y queda en transcripts de chat. Filtrar SIEMPRE.
| Herramienta | Patrón que leakea | Mitigación |
|---|
gh auth status | - Token: gho_<REDACTED> (prefijo parcial enmascarado) | gh auth status 2>&1 | grep -v "^ - Token:" |
git config --list (cuando hay creds en URLs o extraheader) | url.https://USER:<REDACTED>@github.com/..., http.extraheader=AUTHORIZATION: bearer <REDACTED> | git config --list | grep -v "url\.\\|extraheader" |
az account show con credenciales en cache | algunos campos (tenantDefaultDomain, homeAccountId) son fingerprints válidos para correlación | az account show --query "{name:name, id:id, state:state}" -o table (whitelist explícita) |
gh api .../actions/secrets | nombres de secrets son metadata pero pueden filtrar topología | OK exponer nombres si la org/repo es público; redactar en docs externos |
kubectl get secret <name> -o yaml | volca data.<key>: <base64> que es trivialmente decodificable | NUNCA imprimir; usar kubectl get secret <name> -o jsonpath='{.metadata.creationTimestamp}' o describir solo metadata |
docker inspect <container> | Config.Env lista todas las env vars con valor | docker inspect <c> --format '{{range .Config.Env}}{{println (index (split . "=") 0)}}{{end}}' (solo nombres) |
systemctl show <unit> | Environment=KEY=VALUE con valor | systemctl show <unit> --property=Environment | sed 's/=[^[:space:]]*/=<REDACTED>/g' o filtrar nombres con awk como en regla 8 |
Regla general: si una herramienta oficial imprime "parte" del secreto por conveniencia de UX, tratar ese prefijo como secreto completo. Mitigar con un pipe de filtrado documentado aquí; agregar entradas nuevas a esta tabla cuando se descubran.
Origen de esta sección: task O7c (.agents/tasks/2026-05-08-O7c-gh-auth-status-leak-pattern.md en umbral-agent-stack), durante el smoke O7 (PR #378) Copilot-VPS observó el leak de gh auth status y no había mitigación documentada.
Ejemplos en esta tabla: todos usan placeholder <REDACTED>. Nunca pegar tokens reales (ni siquiera enmascarados) como ejemplo.
Triage de hallazgos: real vs falso positivo
No todo match del guardrail es un leak. Cada hallazgo debe pasar por triage antes de decidir qué hacer.
Si el hallazgo es un leak real
- Rotar la credencial afectada en su sistema de origen (Azure, GitHub, Notion, etc.).
- Redactar el output donde apareció: reemplazar con
<REDACTED> en logs, PRs, evidencia y memoria de chat si es posible.
- Reportar a David con: qué secreto, dónde apareció, ya rotado sí/no, qué superficies pudieron leerlo.
- Abrir issue o task de seguimiento para verificar que no quedó cacheado en otro lado (CI logs, artifacts, screenshots).
Si el hallazgo es un falso positivo
Documentar explícitamente (no silenciar sin justificación):
- (a) Patrón que matcheó — qué regla del guardrail disparó (ej.
sk-..., ghp_..., prefijo eyJ).
- (b) Origen del valor — por qué no es un secreto activo (placeholder en docs, ejemplo de la propia skill, hash público, fixture de test, token expirado/revocado y verificado como tal).
- (c) Evidencia de que no es secreto activo — link al doc fuente, commit que introdujo el placeholder, o verificación de revocación.
Regla dura
Nunca silenciar el guardrail sin justificación escrita. Si no podés producir (a) + (b) + (c) para un falso positivo, tratá el hallazgo como leak real hasta demostrar lo contrario. Inclinar la duda hacia rotar/redactar es siempre más barato que asumir que era un placeholder y equivocarse.
What this skill does NOT do
- No reemplaza un escáner real (
gitleaks, trufflehog). Es un guardrail conductual.
- No es una garantía — los humanos siguen siendo la última defensa.
- No se aplica a runtime real de aplicaciones (donde los secrets sí existen y se manejan via env vars / key vault). Aplica solo a outputs visibles para humanos.
Sugerencia futura
Cuando el repo tenga >50 commits con outputs estructurados, evaluar instalar gitleaks como pre-commit hook (policies/05 §"Live agent contract changes" implica formalizar esto en pre-commit-config.yaml).
Cómo verificar que las copias mirror están en sync
Esta skill vive en 4 ubicaciones que deben coincidir byte-a-byte:
<notion-governance>/.agents/skills/secret-output-guard/SKILL.md — canonical (SoT del script check_skill_mirrors.py)
~/.copilot/skills/secret-output-guard/SKILL.md
~/.codex/skills/secret-output-guard/SKILL.md
<umbral-agent-stack>/.agents/skills/secret-output-guard/SKILL.md
(<notion-governance>/skills/secret-output-guard/SKILL.md y notion-governance-skills/secret-output-guard/SKILL.md en umbral-skills-registry son stub-pointer, NO copias mirror — no editar.)
Para detectar drift:
python C:\GitHub\umbral-agent-stack\scripts\maintenance\check_skill_mirrors.py
Para sincronizar mirrors drifteadas desde la canonical:
python C:\GitHub\umbral-agent-stack\scripts\maintenance\check_skill_mirrors.py --fix
Origen: O7c (2026-05-08). Después de F-INC-001 + O7c se descubrió que las copias en los repos habían drifteado silenciosamente. El script check_skill_mirrors.py previene recurrencia.