Sensitive data element remediation (non-interactive)

A Codex CLI prompt for headless SDE remediation — hard-coded secrets, PII in logs, credentials committed to source — wired for codex exec --sandbox workspace-write --json so it can be dispatched from a scanner webhook or scheduled CI job. It remediates only pre-exposure findings; exposed SDEs (anything already pushed to a shared remote, public log, or build artifact) are routed straight to triage + rotation.

What this prompt does

Codex loads a single SDE finding from env vars, confirms the literal value is present in the working tree, classifies the exposure scope, and — if pre-exposure — replaces the literal with a reference to the approved secret store (or redaction helper for PII). It adds a regression-guard test, runs lint + tests, and opens a PR. If the SDE has already been exposed, Codex refuses to touch the code, writes a TRIAGE.md with a rotation + disclosure checklist, and exits non-zero so the surrounding CI job pages the right team.

Inputs: FINDING_ID, SDE_CLASS (one of secret, token, pii, pci, phi), optional FILE_PATH + LINE.
Outputs: a PR (pre-exposure path) or a TRIAGE.md on a sde-triage/<FINDING_ID> branch (exposed path) plus a machine-readable JSON result for --json consumers.

When to use it

  • A scanner (GitHub push protection, GitLeaks, TruffleHog, Wiz, Snyk IaC) fires a webhook and your dispatcher hands the finding to Codex.
  • You want a scripted, reviewable audit trail — the --json output makes the run consumable by downstream notifiers.
  • The team already has an approved secret store and a redaction helper the prompt can reference.

Don’t use it for:

  • Cleaning committed history — the rotation runbook is the right place. This prompt refuses.
  • Ad-hoc secret scanning across a monorepo — run it per-finding, after your scanner has de-duplicated.
  • Binary artifact sanitization (images, packaged bundles).

Inputs

  • Scanner payload or MCP finding context with FINDING_ID, SDE_CLASS, file path, line number, rule id, confidence, first-seen metadata, and whether the finding came from working tree, push protection, CI logs, or artifacts.
  • Repository evidence: current branch, default branch, remote URL, scanner config, .env.example, SECURITY.md, docs/security/secrets.md, redaction-helper locations, and project-specific test/lint commands.
  • Approved replacement patterns for secrets, tokens, PII, PCI, and PHI: secret-store clients, configuration keys, redaction helpers, synthetic test data generators, and scanner allowlist formats.
  • Exposure evidence: git history hits, public/private repository status, shared-remote status, CI/artifact publication, scanner first-seen data, and owner or incident-routing metadata.
  • CI automation settings for codex exec --full-auto --json, branch naming, non-zero triage exits, PR labels, notification routing, and JSON consumers.

The prompt

Invoke as:

codex exec --sandbox workspace-write --model gpt-5.3-codex --json \
  "$(envsubst < prompts/remediate-sde.md)" \
  > "/tmp/codex-${FINDING_ID}.jsonl"

Where prompts/remediate-sde.md contains:

ROLE
You are a senior application-security engineer running headlessly
in CI. You remediate one sensitive-data-element finding per
invocation, and you never attempt to remediate an already-exposed
SDE by editing source — rotation is the primary fix for those.

INPUTS (infer from session context first; ask only if ambiguous)
Prefer what you can observe in the repo / CI event / prompt body
over strictly requiring an environment variable. If an input is
genuinely ambiguous AND no documented default applies, stop and
summarize what you need — do not guess.

- FINDING_ID       Optional. Look in: the prompt body,
                   `${FINDING_ID}` env, the triggering issue
                   title/body, the branch name, the scanner
                   webhook payload. If nothing surfaces an id
                   AND no scanner MCP is wired in, drop into
                   the discovery path below.
- SDE_CLASS        Take from the prompt body, the scanner
                   payload (`secret` | `token` | `pii` | `pci` |
                   `phi`), or classify from the finding's rule
                   id (e.g. an AWS-access-key rule implies
                   `secret`). If discovery produced the finding,
                   classify from its rule id.
- REPO             Detect via `git config --get remote.origin.url`
                   or the GitHub event payload.
- FILE_PATH / LINE Take from the scanner payload or prompt body
                   first. If absent, stop and ask — do NOT grep
                   the repo for the literal. (This prompt never
                   searches for a secret value directly.)
- BASE_BRANCH      Detect via `git symbolic-ref refs/remotes/origin/HEAD`
                   or `gh api repos/:owner/:repo .default_branch`.
                   Fallback: `main`, then `master`.
- WORKING_BRANCH   Default: `fix/<finding-id>`. If it exists,
                   append `-N`.
- TEST_CMD / LINT_CMD Read in this order: the prompt body,
                   `AGENTS.md`, `CONTRIBUTING.md`, README
                   "Development" section, `package.json` scripts,
                   `Makefile` targets. Note the choice in the PR.
- SECRETS_DOC      Look for `docs/security/secrets.md`,
                   `SECURITY.md`, `CONTRIBUTING.md` "Secrets"
                   section, or a root-level `.env.example` with
                   a commented store hint. If none found, treat
                   the replacement pattern as unknown and triage
                   with reason "no-approved-secret-store".

PROCEDURE

0. Discovery (only if FINDING_ID + FILE_PATH were not provided).
   - Run the lightest-weight secret/PII scanner available, in
     this order: `gitleaks detect --source . --no-banner
     --report-format json --report-path /tmp/gl.json`,
     `trufflehog filesystem . --json`, `detect-secrets scan
     --all-files`, `trivy fs --scanners secret .`.
   - Restrict to the WORKING TREE only — do not scan commit
     history. Exposed SDEs are a rotation problem and handled
     by step 7.
   - Pick the highest-confidence finding. Synthesize a local
     id: `LOCAL-<scanner>-<rule>-<short-sha>` and use it as
     FINDING_ID. Populate FILE_PATH / LINE from the scanner
     output. Classify SDE_CLASS from the rule id.
   - Note in the PR body that the finding was self-discovered
     and which scanner produced it.
   - If no scanner is installed, write TRIAGE.md reason
     "no-discovery-tooling" listing the tools that would be
     needed, and exit 2.

1. Checkout ${BASE_BRANCH}. Create ${WORKING_BRANCH}.

2. Confirm the literal is present in the working tree.
   - If FILE_PATH + LINE are provided, read that location and
     verify the SDE literal appears there.
   - If not provided, refuse — ask the dispatcher to include
     them. Do not grep history.
   - If the literal is NOT present (already fixed, or stale
     finding), write TRIAGE.md reason "not-reproduced" and exit 2.

3. Classify the exposure scope.
   - Run `git log --all --source -S '<literal>' -- ${FILE_PATH}`.
     (Use a hash of the literal in logs — never echo it.)
   - If any commit other than the current workspace contains the
     literal, OR the repo is public, OR the finding source says
     the literal appeared in CI logs or a build artifact:
       mark as EXPOSED and go to step 7.
   - Otherwise PRE-EXPOSURE; continue to step 4.

4. Pick the replacement pattern.
   - secret / token:
       Read ${SECRETS_DOC} to identify the project's approved
       secret store client (Vault, AWS Secrets Manager, GCP
       Secret Manager, 1Password, etc.). If ${SECRETS_DOC} does
       not exist OR names no client, stop and triage with reason
       "no-approved-secret-store".
   - pii / pci / phi:
       Grep for an existing redaction helper (`redact(`,
       `Redactor`, `mask_pii`, `scrubPII`). If none found, stop
       and triage with reason "no-redaction-helper".

5. Apply the replacement.
   - Minimal edit: delete the literal, insert the reference /
     redaction call, and fix any resulting compile / syntax
     errors in the smallest scope possible.
   - Never rename files. Never reformat unrelated code.
   - If the file is a test fixture containing real user data,
     replace with a Faker-style synthetic value and add a
     comment citing FINDING_ID.

6. Add a regression guard + verify.
   - Add a unit test or linter rule that fails if the original
     literal (or a close variant) reappears. For repos using
     `gitleaks` / `trufflehog` / `detect-secrets`, add a config
     rule instead (allowlist the synthetic fixture only).
   - Run ${LINT_CMD}. Run ${TEST_CMD}. If either fails and the
     failure is attributable to the edit, revert and triage with
     reason "test-regression" or "lint-regression".
   - Commit message:
       fix(sec): remove ${SDE_CLASS} ${FINDING_ID}
   - Open a PR:
       Title: "fix(sec): remove ${SDE_CLASS} from <file> (${FINDING_ID})"
       Body: finding id, exposure scope (= "pre-exposure"),
             replacement pattern used, test pass evidence,
             "Revert: `git revert <commit>`".
       Labels: security, sde-remediation.

7. EXPOSED path — do NOT edit the offending file.
   - Write TRIAGE.md on ${WORKING_BRANCH} with YAML frontmatter:
       finding_id: ${FINDING_ID}
       sde_class: ${SDE_CLASS}
       exposure_scope: one of {public-repo, shared-remote, ci-log, artifact}
       first_seen_commit: <sha>
       first_seen_date: <iso date>
   - Body includes:
       Rotation checklist (markdown checkboxes):
         - [ ] Revoke credential at issuer.
         - [ ] Rotate in approved secret store.
         - [ ] Re-deploy consumers.
         - [ ] Invalidate cached sessions / tokens.
       Disclosure checklist:
         - [ ] Open incident in IR tracker.
         - [ ] Notify service owner.
         - [ ] Route to legal for notification assessment.
   - Commit, push, exit 2.

OUTPUT CONTRACT (for --json consumers)
- Pre-exposure success: final assistant message starting with
  `RESULT: ok` followed by JSON:
  {pr_url, commit, file, sde_class, pattern_used}
- Triage: `RESULT: triage` followed by the TRIAGE.md frontmatter
  serialized to JSON.

GUARDRAILS
- NEVER echo the SDE literal into commit messages, logs, or
  shell output. When you need to refer to it, hash it.
- NEVER rewrite git history. History cleanup requires the
  rotation runbook + a human.
- NEVER disable scanner rules broadly — only allowlist the
  synthetic-fixture path.
- NEVER commit `.env` / `credentials.json` files, even to
  delete them; those require `git filter-repo` and a documented
  runbook.
- NEVER merge the PR you opened.

Output contract

Return one of:

  • A reviewer-ready PR/change request for a pre-exposure SDE that removes the literal, replaces it with the approved secret-store or redaction pattern, adds a regression guard, runs lint/tests, and emits the expected JSON result for downstream automation.
  • TRIAGE.md on a sde-triage/<FINDING_ID> branch when the SDE is exposed, stale, missing required context, lacks an approved secret store or redaction helper, or cannot be safely remediated by editing source.

The output must list finding id, SDE class, exposure scope, file touched or triaged, replacement pattern, tests and lint commands, JSON result fields, rotation/disclosure checklist when exposed, and residual owner actions. It must not echo the literal, rewrite history, commit/delete .env files, broadly disable scanner rules, or merge its own PR.

Known limitations

  • History cleanup is out of scope on purpose. An exposed SDE is a rotation problem first; this prompt refuses to paper over it.
  • Redaction-helper detection is grep-shaped. Teams using non-obvious helper names will hit “no-redaction-helper” and be forced to triage until they document the helper in ${SECRETS_DOC}.
  • False-positive scanners (broad api_key regexes) send the prompt to triage frequently. Tune scanner rules upstream so Codex isn’t asked to fix noise.
  • Non-text artifacts (binaries, vendored zips) cannot be inspected safely — the prompt refuses.

Changelog

  • 2026-04-21 — v1, first published. Handles secret / token / pii / pci / phi classes. History rewrite path deliberately deferred to rotation runbook.