Skip to content

Redaction

redact

Redact secrets from text before it is recorded or shown.

Command output from a sandbox (the tails of setup and validate steps) ends up in run records, bot comments and job logs. :func:redact replaces what looks like a credential with :data:REDACTED:

  • the value of an Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key or Api-Key header, and a Bearer or Basic credential anywhere;
  • the user information of a URL (https://user:token@host becomes https://[REDACTED]@host);
  • tokens with a well-known shape: OpenAI and Anthropic keys, GitHub, GitLab, npm and Slack tokens, AWS access key ids, Google API keys and OAuth access tokens, JSON Web Tokens and OpenShell credential placeholders;
  • the value in NAME=value or "name": "value" when the name looks like a credential (token, secret, password, key, credential, auth, cookie or the word pat);
  • every literal value given in secrets, such as the credentials the host holds (:func:secret_values).

It is a best-effort filter for accidental leaks, not a guarantee: a secret with no recognizable shape that the host does not hold passes through.

is_secret_name(name)

Whether the variable name looks like it holds a credential.

Source code in src/agentic_ci/redact.py
def is_secret_name(name: str) -> bool:
    """Whether the variable *name* looks like it holds a credential."""
    return bool(_SECRET_NAME_RE.fullmatch(name))

secret_values(env)

The values of env whose names look like credentials, longest first.

Values shorter than :data:MIN_SECRET_LENGTH are left out, as are values that are not strings. Surrounding whitespace is stripped, so a key read from a file with a trailing newline still matches. Each line of a multi-line value (a PEM key, a JSON credential) is included too, because step tails are redacted line by line.

Source code in src/agentic_ci/redact.py
def secret_values(env: Mapping[str, str]) -> tuple[str, ...]:
    """The values of *env* whose names look like credentials, longest first.

    Values shorter than :data:`MIN_SECRET_LENGTH` are left out, as are values
    that are not strings. Surrounding whitespace is stripped, so a key read
    from a file with a trailing newline still matches. Each line of a
    multi-line value (a PEM key, a JSON credential) is included too, because
    step tails are redacted line by line.
    """
    values: set[str] = set()
    for name, value in env.items():
        if not (isinstance(value, str) and is_secret_name(name)):
            continue
        for part in (value, *value.splitlines()):
            values.add(part.strip())
    return tuple(sorted((v for v in values if len(v) >= MIN_SECRET_LENGTH), key=len, reverse=True))

redact(text, secrets=())

Return text with credentials replaced by :data:REDACTED (see the module docstring).

Source code in src/agentic_ci/redact.py
def redact(text: str, secrets: Iterable[str] = ()) -> str:
    """Return *text* with credentials replaced by :data:`REDACTED` (see the module docstring)."""
    for secret in sorted(
        {s for s in secrets if isinstance(s, str) and len(s) >= MIN_SECRET_LENGTH},
        key=len,
        reverse=True,
    ):
        text = text.replace(secret, REDACTED)
    for pattern, replacement in _PATTERNS:
        text = pattern.sub(replacement, text)
    return text