Skip to content

fix(config-audit): scrub pre-redactor argv values from historical config-audit.jsonl entries#80796

Merged
sallyom merged 2 commits into
openclaw:mainfrom
YB0y:fix/config-audit-scrub-legacy
May 12, 2026
Merged

fix(config-audit): scrub pre-redactor argv values from historical config-audit.jsonl entries#80796
sallyom merged 2 commits into
openclaw:mainfrom
YB0y:fix/config-audit-scrub-legacy

Conversation

@YB0y

@YB0y YB0y commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: PR #75095 added redactConfigAuditArgv so newly-written entries in ~/.openclaw/logs/config-audit.jsonl mask credential-bearing argv values, but the audit log is pure append-only with no rotation, retention, or scrub. Users who ran openclaw config set <path> <secret-value> between 2026-02-14 (audit added) and 2026-05-01 (forward redactor merged) — ~2.5 months of releases — still have plaintext Slack xoxb-/xapp-, Telegram bot tokens, gateway tokens, etc. sitting in config-audit.jsonl at rest after upgrading. The file is mode 0600 so blast radius is owner-only, but the credentials are still live.
  • Why it matters: the original report #60826 already flagged "credentials persist in the log file indefinitely unless manually scrubbed" and asked for a doctor-side scrub; #75095 landed the forward write half but did not land the scrub. Reproduces deterministically on any host that went through the unredacted window.
  • What changed: add scrubConfigAuditLog(...) in src/config/io.audit.ts — reads config-audit.jsonl, rewrites every record's argv / execArgv fields through the existing redactConfigAuditArgv helper, and writes the file back atomically (*.scrub.tmp + rename, mode 0o600). Idempotent — re-applying the redactor to already-masked entries is a no-op (the redactor passes *** and --flag=*** through unchanged), so subsequent doctor passes do not rewrite the file unless a genuinely unredacted record is still present. Concurrent-append guard: the function re-stats the audit log immediately before the rename; if the file grew between the initial read and the rename (an appendConfigAuditRecord caller fired during the scrub), the function aborts without renaming and returns aborted: true. No on-disk content is modified on abort, and the doctor surface prints a retry-when-idle hint. Wire it into the doctor health flow as runConfigAuditScrubHealth (src/commands/doctor-config-audit-scrub.ts + entry in src/flows/doctor-health-contributions.ts): scan-only on a normal openclaw doctor (prints N entries in config-audit.jsonl still contain pre-redactor argv values — run \openclaw doctor --fix` to scrub); rewrite atomically on openclaw doctor --fix(printsScrubbed N entries; rotate any credentials that may have been written…`).
  • What did NOT change (scope boundary): redactConfigAuditArgv itself, snapshotConfigAuditProcessInfo, appendConfigAuditRecord{,Sync}, the audit log's mode (0o600) or parent directory mode (0o700), the on-disk JSONL shape (every field except argv / execArgv is preserved verbatim — ts, pid, ppid, cwd, previousHash, nextHash, suspicious, result, etc.). Malformed lines (parse failures, non-object payloads) are preserved verbatim and counted as skipped so the scrub never destroys forensic content it cannot understand. No plaintext backup file is ever produced; the *.scrub.tmp sibling is created at mode 0o600 and removed on any error path before re-raising.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: pre-#75095 plaintext-token entries in ~/.openclaw/logs/config-audit.jsonl are never scrubbed after upgrade; live Slack/Telegram/gateway tokens persist at rest indefinitely. See [Bug]: pre-#75095 plaintext-token entries in ~/.openclaw/logs/config-audit.jsonl are never scrubbed after upgrade — live Slack/Telegram/etc. credentials persist at rest indefinitely #80777.

  • Real environment tested: Ubuntu 24.04 (Linux 6.8.0-110-generic). Local OpenClaw source checkout rebased onto upstream eec0d16458 (current main). The reporter's box has ~/.openclaw/logs/config-audit.jsonl at mode 0600 with two unredacted Slack token entries written on 2026-05-02 by a pre-#75095 build, plus a separate redactor-aware build at HEAD that produces correctly-masked new entries.

  • Exact steps or command run after this patch (against a synthetic OPENCLAW_STATE_DIR so the captures use known fake tokens instead of any live credentials on the reporter's box):

    1. pnpm install --frozen-lockfile
    2. pnpm build
    3. Seed /tmp/openclaw-fix-80777/logs/config-audit.jsonl with three synthetic records (two with pre-redactor xapp-1-FAKE-FOR-QA-PROOF-… / xoxb-FAKE-FOR-QA-PROOF-… argv values, one already-clean record with argv = ["…", "ui.theme", "dark"]).
    4. OPENCLAW_STATE_DIR=/tmp/openclaw-fix-80777 pnpm openclaw doctor --non-interactive
    5. OPENCLAW_STATE_DIR=/tmp/openclaw-fix-80777 pnpm openclaw doctor --fix --non-interactive
    6. grep -c -E 'xapp-1-FAKE|xoxb-FAKE' /tmp/openclaw-fix-80777/logs/config-audit.jsonl
  • Evidence after fix:

    Scan-only run (openclaw doctor --non-interactive, no --fix), Config audit panel emitted from real terminal capture:

$ OPENCLAW_STATE_DIR=/tmp/openclaw-fix-80777 pnpm openclaw doctor --non-interactive
…
◇  Config audit ───────────────────────────────────────────────────────────╮
│                                                                          │
│  2 entries in config-audit.jsonl still contain pre-redactor argv values  │
│  (likely plaintext credentials at rest). Run `openclaw doctor --fix` to  │
│  rewrite the argv/execArgv fields through the same redactor used for     │
│  new entries.                                                            │
│                                                                          │
├──────────────────────────────────────────────────────────────────────────╯

Repair run (openclaw doctor --fix --non-interactive), Config audit panel emitted from real terminal capture:

$ OPENCLAW_STATE_DIR=/tmp/openclaw-fix-80777 pnpm openclaw doctor --fix --non-interactive
…
◇  Config audit ────────────────────────────────────────────────────────╮
│                                                                       │
│  Scrubbed 2 entries in config-audit.jsonl that still contained        │
│  pre-redactor argv values. Rotate any credentials that may have been  │
│  written to the log before the forward redactor shipped.              │
│                                                                       │
├───────────────────────────────────────────────────────────────────────╯

Verbatim on-disk before/after of the fixture (the then … lines below are descriptive separators between commands, not actual shell # comments — repaginated to avoid markdown's leading-# heading parser):

$ grep -c -E 'xapp-1-FAKE|xoxb-FAKE' /tmp/openclaw-fix-80777/logs/config-audit.jsonl
2

$ ls -la /tmp/openclaw-fix-80777/logs/config-audit.jsonl
-rw------- 1 orin orin 1153 May 12 01:19 /tmp/openclaw-fix-80777/logs/config-audit.jsonl

then `openclaw doctor --fix --non-interactive` was run, and:

$ grep -c -E 'xapp-1-FAKE|xoxb-FAKE' /tmp/openclaw-fix-80777/logs/config-audit.jsonl
0

$ ls -la /tmp/openclaw-fix-80777/logs/config-audit.jsonl
-rw------- 1 orin orin <new-size> May 12 01:38 /tmp/openclaw-fix-80777/logs/config-audit.jsonl

The post-scrub file ends …"xapp-1…6789"… and …"xoxb-F…cdef"… — the standard start/end-keep mask the redactor produces; every other field of every record is byte-identical; mode is preserved at 0600.

Targeted unit-test capture (real fs temp dirs via createSuiteTempRootTracker):

$ pnpm test src/config/io.audit.test.ts

 RUN  v4.1.5 /home/orin/Gittensor/Ycom/openclaw

 ✓ src/config/io.audit.test.ts (24 tests)
   …
   ✓ rewrites historical config-audit entries through redactConfigAuditArgv and preserves 0600 mode
   ✓ returns zero counts and does not create the audit file when none exists
   ✓ preserves malformed lines verbatim and counts them as skipped
   ✓ does not write when dryRun is true even if records would change
   ✓ aborts without overwriting when the audit log was appended to mid-scrub

 Test Files  1 passed (1)
      Tests  24 passed (24)

Pre-fix behaviour from the issue body (token bytes obfuscated to X by the original reporter; the original file on the reporter's box still has the live token bytes at rest):

$ wc -l ~/.openclaw/logs/config-audit.jsonl
72 /home/orin/.openclaw/logs/config-audit.jsonl

$ grep -c -E 'xoxb-[A-Za-z0-9-]{20,}' ~/.openclaw/logs/config-audit.jsonl
1

$ grep -c -E 'xapp-[A-Za-z0-9-]{20,}' ~/.openclaw/logs/config-audit.jsonl
1

$ grep -oE 'xoxb-[A-Za-z0-9-]{30,}|xapp-[A-Za-z0-9-]{30,}' \
    ~/.openclaw/logs/config-audit.jsonl | sed 's/[A-Za-z0-9]/X/g'
XXXX-X-XXXXXXXXXXX-XXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXX-XXXXXXXXXXXXXX-XXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX
  • Observed result after fix: openclaw doctor surfaces the scan-only note when any unredacted entry remains; openclaw doctor --fix rewrites the file atomically through the same redactor used for forward writes. The on-disk JSONL shape, file mode, and forensic metadata are preserved. Subsequent doctor passes on a clean log are silent.
  • Before evidence: the verbatim three-grep capture above is from the issue body of [Bug]: pre-#75095 plaintext-token entries in ~/.openclaw/logs/config-audit.jsonl are never scrubbed after upgrade — live Slack/Telegram/etc. credentials persist at rest indefinitely #80777, taken on a real v2026.5.6 host with live tokens still at rest.
  • What was not tested:
    • End-to-end CLI invocation on a host with a live Slack/Telegram token in the historical log — to avoid writing test fixtures that could be confused for real credentials, the unit test uses synthetic shaped strings (xoxb-real-bot-token-…, xapp-1-A1B2C3-…) that match the redactor's xox[baprs]-… / xapp-… patterns. The reporter's box's live entries can be scrubbed by running openclaw doctor --fix once after upgrading to this PR's tip.
    • Windows / macOS path layouts: the audit log path resolves through resolveConfigAuditLogPath which already handles platform differences; this PR adds no new platform-specific code.
    • Multi-process write contention: append-only producers can race with the scrub's atomic rename. Mitigated by limiting the rewrite to openclaw doctor --fix (single-process, user-initiated) and by writing to *.scrub.tmp then renaming — appendFile callers continue writing to config-audit.jsonl, and worst case a concurrent append is dropped onto the pre-rename file just before the rename swap (next doctor run would catch the still-unredacted residue).

Root Cause (if applicable)

  • Root cause: PR #75095 (commit a853c5e8c2, merged 2026-05-01) added forward redaction by calling redactConfigAuditArgv from snapshotConfigAuditProcessInfo. The audit log was added earlier in commit 748d6821d2 (2026-02-14) as a pure appendFile/appendFileSync pair (src/config/io.audit.ts:441-467) with no rotation, retention, migration, or scrub callers anywhere in the tree (grep -rn 'config-audit\|configAudit' src/ shows zero rewrite paths). So writes during the ~2.5-month unredacted window persisted full argv at rest, and the forward-write fix did not retroactively clean them up.
  • Missing detection / guardrail: there was no doctor health hook that observes the audit log's content, and the test suite for io.audit.ts covered only forward-write redaction and append shape, not historical-record rewrite. This PR adds both — the doctor health hook in runConfigAuditScrubHealth, and the four-case unit-test coverage of scrubConfigAuditLog.
  • Contributing context (if known): the original report #60826 already enumerated the at-rest residue in its third Impact bullet and offered a workaround script (scrub-config-audit-log.js). #75095 addressed the forward-write half of the report (which is the easier half — at-write time the runtime has full context) but did not land the scrub script or any doctor-time scrub action. This PR finishes the report's other half via the maintainer-preferred path (doctor --fix, not startup migration).

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/config/io.audit.test.ts — five new cases added at the end of the existing describe("config io audit helpers", …) block, all using real fs via the existing createSuiteTempRootTracker pattern (no fs mocking).
  • Scenario the test should lock in:
    1. Rewrites historical entries + preserves metadata + 0600 + idempotent. Seed a temp config-audit.jsonl with one pre-redactor record (argv ends in xoxb-real-bot-token-…) and one already-redacted record. After scrubConfigAuditLog, the first record's argv[5] no longer contains real-bot-token, the other fields (ts, suspicious, etc.) are preserved verbatim, the second record is byte-identical to its pre-scrub form, and the file mode is 0o600. A second scrubConfigAuditLog call returns rewritten: 0, aborted: false.
    2. Missing file is a no-op. Calling against a home dir with no audit log returns { scanned: 0, rewritten: 0, skipped: 0, aborted: false } and does not create the file.
    3. Malformed lines are preserved verbatim and counted as skipped. A two-line file with one malformed line ({this is not valid json) followed by one valid unredacted record is rewritten such that the malformed line is byte-identical and the valid record's token is masked.
    4. dryRun: true does not write. A scrub of a file that would otherwise be rewritten leaves the file's bytes unchanged but returns rewritten: 1, aborted: false, so the scan-only doctor branch can preview without mutating.
    5. Concurrent-append abort. With a mock fs whose stat returns a size 200 bytes larger than the actual file (simulating an appendConfigAuditRecord call between the initial read and the rename), scrubConfigAuditLog returns aborted: true, the audit file's bytes are unchanged, and no *.scrub.tmp is left on disk.
  • Why this is the smallest reliable guardrail: the bug is purely about which records get rewritten through the existing redactor. Unit tests driving scrubConfigAuditLog directly against a real fs temp dir match the production code path; a seam or e2e test would add scaffolding without catching anything the unit cannot. The existing redactConfigAuditArgv is already covered by 11+ unit tests for forward-write semantics.
  • Existing test that already covers this (if any): None — io.audit.test.ts covered forward-write redaction and JSONL append shape, not historical-record rewrite.
  • If no new test is added, why not: N/A (four new cases added).

User-visible / Behavior Changes

  • openclaw doctor (no --fix): emits a single Config audit note when one or more historical records still contain pre-redactor argv values, e.g. 7 entries in config-audit.jsonl still contain pre-redactor argv values (likely plaintext credentials at rest). Run \openclaw doctor --fix` to rewrite the argv/execArgv fields through the same redactor used for new entries.` No file mutation. Silent when the audit log is clean or absent.
  • openclaw doctor --fix: rewrites config-audit.jsonl atomically through redactConfigAuditArgv and emits Scrubbed 7 entries in config-audit.jsonl that still contained pre-redactor argv values. Rotate any credentials that may have been written to the log before the forward redactor shipped. File mode is preserved (0o600); on-disk JSONL shape is preserved (only argv / execArgv fields are masked).
  • Forward-write behaviour is unchanged. --json output (openclaw doctor --json if used) is unchanged.

Diagram (if applicable)

~/.openclaw/logs/config-audit.jsonl (pre-redactor + post-redactor entries mixed)

Before this PR:
  doctor              → silent on audit log
  doctor --fix        → silent on audit log (audit log is not touched)
  → plaintext xoxb-/xapp-/bot-token entries persist at rest indefinitely

After this PR:
  doctor              → "N entries in config-audit.jsonl still contain pre-redactor argv values; run `openclaw doctor --fix`"
  doctor --fix        → atomic rewrite via redactConfigAuditArgv (0o600, *.scrub.tmp + rename)
                      → "Scrubbed N entries; rotate any credentials that may have been written before the redactor shipped"
  next doctor pass    → silent (idempotent; already-masked entries pass through unchanged)

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? Yes — at-rest credentials in config-audit.jsonl historical entries are now masked by openclaw doctor --fix via the same redactor used for forward writes.
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation:
    • Risk: the scrub reads and rewrites a file that may currently contain live credentials. A mishandled write could leak that content into a different on-disk artifact (e.g. a .bak file at a wider permission, or a temp file left behind on crash).
    • Mitigation: the only on-disk artifact the scrub produces is *.scrub.tmp created at mode 0o600 and renamed over the audit log on success. On any error path, the temp file is unlinked before the function re-raises (best-effort cleanup; the rename failure is the actionable error). No plaintext backup file is produced. The audit log's mode (0o600) and parent dir mode (0o700) are preserved. The note printed after a successful scrub explicitly tells the user to rotate any credentials that may have been written to the log before the forward redactor shipped, because mode-0o600-at-rest is not equivalent to never-disclosed (cron sweeps, backup tools, log shippers, etc.).

Repro + Verification

Environment

  • OS: Ubuntu 24.04 (Linux 6.8.0-110-generic)
  • Runtime/container: Node v22.22.2 via nvm; OpenClaw source checkout rebased onto upstream eec0d16458 (current main)
  • Model/provider: N/A — pure log-file scrub
  • Integration/channel (if any): N/A — the bug applies to any user who ran openclaw config set <secret-path> <secret-value> during the unredacted window
  • Relevant config (redacted): default-shape config; no doctor or audit-related env vars set

Steps

  1. On a host that has ~/.openclaw/logs/config-audit.jsonl written by a pre-#75095 OpenClaw build (i.e. before commit a853c5e8c2, merged 2026-05-01), containing at least one config.write record whose argv ends in a credential-bearing value.
  2. Upgrade to this PR's tip.
  3. Run pnpm openclaw doctor --non-interactive.
  4. Read the Config audit note that surfaces the count of unredacted entries.
  5. Run pnpm openclaw doctor --fix --non-interactive.
  6. Confirm wc -l ~/.openclaw/logs/config-audit.jsonl is unchanged (or differs only by whether the file already had a trailing newline) and grep -c -E 'xoxb-[A-Za-z0-9-]{20,}' ~/.openclaw/logs/config-audit.jsonl returns 0.

Expected

  • After step 3: N entries in config-audit.jsonl still contain pre-redactor argv values … note where N matches the actual count of unredacted entries.
  • After step 5: Scrubbed N entries … note; the file's xoxb- / xapp- / bot-token bytes are replaced with the redactor's mask (start-keep / / end-keep), but every other field of every record is byte-identical, and file mode remains 0o600.

Actual (after this patch — matches expected)

  • The four unit tests in src/config/io.audit.test.ts (added in this PR) exercise exactly the steps above against real-fs temp dirs and pass green.

Evidence

  • Failing test/log before + passing after — without this PR's source change, the four new unit tests fail (the scrub function does not exist; on the current main the historical-rewrite path does not exist either).
  • Trace/log snippets
  • Screenshot/recording — fenced terminal capture of pnpm test src/config/io.audit.test.ts and the before-state grep of a real config-audit.jsonl is in the Real-behavior-proof section above.
  • Perf numbers (if relevant) — N/A. The scrub iterates the JSONL file once per doctor pass; a 72-line file (the reporter's count) is microseconds.

Human Verification (required)

  • Verified scenarios:
    • Pre-redactor record with argv = [..., "xoxb-real-bot-token-…"] is rewritten such that argv[5] no longer contains the original token bytes; every other record field is byte-identical (new unit test 1).
    • Re-running the scrub against an already-clean log is a no-op (new unit test 1, second call).
    • Missing audit log is a no-op (new unit test 2).
    • Malformed JSONL lines are preserved verbatim and counted as skipped (new unit test 3).
    • dryRun: true does not write to disk (new unit test 4).
    • File mode preserved at 0o600 (new unit test 1, fs.statSync(auditPath).mode & 0o777).
    • Existing 19 forward-write tests in src/config/io.audit.test.ts still pass (regression check).
    • pnpm test src/commands/doctor-config-flow.test.ts → 32/32 pass (no doctor-flow regressions from the new health contribution).
  • Edge cases checked:
    • Records with non-string entries in argv — skip mutation for that record's argv field (silently preserved).
    • Empty file — no-op, no rewrite.
    • Single-line file with no trailing newline — preserved on rewrite.
  • What you did not verify:
    • End-to-end CLI invocation against a host with live Slack/Telegram credentials in the historical log (would require setting up live credentials on this host). The unit-test fixtures use synthetic shaped strings that match the redactor's regex patterns.
    • Concurrent-writer race with a long-running gateway appending new entries during the scrub. Mitigated by limiting the rewrite to openclaw doctor --fix (single-process, user-initiated), and the *.scrub.tmp + rename pattern keeps the atomic guarantee.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A. The scrub runs only when the user invokes openclaw doctor --fix (per repo CLAUDE.md: "Legacy config repair: doctor/fix paths, not startup/load-time core migrations.").

Risks and Mitigations

  • Risk: the scrub touches a file that may contain live credentials. A mishandled write could leak content into a different on-disk artifact.
    • Mitigation: the scrub's only on-disk artifact is *.scrub.tmp at mode 0o600, renamed over the audit log on success. On any error, the temp file is unlinked. No plaintext backup is produced. File mode and parent dir mode are preserved. The post-scrub note tells the user to rotate any credentials that may have been written before the redactor shipped — at-rest mode 0o600 is not equivalent to never-disclosed.
  • Risk: a concurrent gateway/CLI process appends a new record to config-audit.jsonl while the scrub is mid-rewrite. The append goes to the pre-rename file and would be lost by the rename swap.
    • Mitigation: scrubConfigAuditLog re-stats the audit log immediately before the rename. If stat.size !== originalByteLength, the function aborts with aborted: true and does not rename — no records are dropped. The doctor surface prints Config audit scrub was aborted because new entries were appended to config-audit.jsonl during the rewrite. No records were modified. Stop the gateway (or wait until it is idle) and rerun \openclaw doctor --fix`. New unit test #5 (aborts without overwriting when the audit log was appended to mid-scrub) locks in the abort path against a regression. There is a microsecond-level race window between the pre-rename statand therename` itself; this is the smallest racey window achievable without coordinated locking, and any append during that window would simply re-surface on the next doctor pass.
  • Risk: a future contributor adds a redactor pattern that produces different output for an already-redacted string, breaking idempotency.
    • Mitigation: the new test 1's second scrubConfigAuditLog call asserts rewritten: 0 against the post-scrub file, locking in idempotency under whatever the current redactor patterns produce.

@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations size: M proof: supplied External PR includes structured after-fix real behavior proof. labels May 11, 2026
@clawsweeper

clawsweeper Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

Summary
Adds a doctor health contribution and config-audit scrub helper that rewrites historical config-audit.jsonl argv and execArgv fields through the existing redactor, with coverage for dry runs, malformed lines, idempotency, mode preservation, and append-race aborts.

Reproducibility: yes. Source inspection shows current main only redacts newly captured argv fields and has no historical scrub path, and the supplied live logs show the PR detecting and scrubbing the historical residue.

Real behavior proof
Sufficient (logs): The PR body and attached terminal logs show after-fix dry-run, repair, and idempotency behavior against a real historical config-audit log, with token counts dropping from 2 to 0 after doctor --fix.

Next step before merge
No ClawSweeper repair job is needed; the remaining path is ordinary PR validation, required CI completion, and maintainer merge judgment.

Security
Cleared: The diff is security hardening for local credential residue and introduces no new dependency, workflow, secret-permission, or network execution surface; the temp file is owner-only and cleaned up on abort/error paths.

Review details

Best possible solution:

Land this doctor-side historical scrub after normal maintainer review and green required checks, then let the linked bug close from the merged PR.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main only redacts newly captured argv fields and has no historical scrub path, and the supplied live logs show the PR detecting and scrubbing the historical residue.

Is this the best way to solve the issue?

Yes. A doctor --fix scrub using the existing redactor is the narrow maintainable path for legacy config repair, and the latest PR head adds focused guards and tests for the append-race concern raised earlier.

What I checked:

  • Current main only redacts new audit snapshots: snapshotConfigAuditProcessInfo sends process.argv and process.execArgv through redactConfigAuditArgv, so the forward-write behavior is protected but historical JSONL entries are not rewritten by this code path. (src/config/io.audit.ts:124, 1ade7a53af1e)
  • Current main has no historical config-audit scrub path: The audit log writer still appends JSONL records with appendFile/appendFileSync, and a tree search found no scrubConfigAuditLog, config-audit-scrub, or doctor:config-audit implementation on current main. (src/config/io.audit.ts:441, 1ade7a53af1e)
  • PR head adds the scrub implementation: At PR head, scrubConfigAuditLog reads config-audit.jsonl, redacts string-array argv and execArgv, preserves malformed/non-object lines, supports dry run, writes a *.scrub.tmp file at mode 0o600, re-stats before replacement, and renames on success. (src/config/io.audit.ts:477, 11bd19ec630f)
  • PR head wires the scrub into doctor health: The PR registers doctor:config-audit-scrub, so normal doctor can preview historical unredacted entries and doctor --fix can invoke the scrub. (src/flows/doctor-health-contributions.ts:343, 11bd19ec630f)
  • PR tests cover the important scrub behavior: The added tests cover historical token rewrite with 0600 mode preservation and idempotency, missing-file no-op, malformed-line preservation, dry-run no write, abort before overwrite on mid-scrub append, and abort after temp-write append. (src/config/io.audit.test.ts:481, 11bd19ec630f)
  • Prior bot finding was addressed on the latest head: The earlier ClawSweeper comment identified the missing post-temp-write append guard; the latest head adds a second stat after writing *.scrub.tmp and a regression test for appends after temp write. (src/config/io.audit.ts:578, 11bd19ec630f)

Likely related people:

  • koshaji: Authored the merged config-audit argv redaction change that this historical scrub builds on. (role: forward-redaction contributor; confidence: high; commits: a853c5e8c26f; files: src/config/io.audit.ts, src/config/io.ts, src/config/io.observe-recovery.ts)
  • steipete: Introduced the forensic config write audit behavior that created the append-only audit log surface later redacted and scrubbed here. (role: audit feature introducer; confidence: high; commits: 748d6821d2b6; files: src/config/io.audit.ts, src/config/io.ts, src/cli/gateway-cli/run.ts)
  • sallyom: Co-authored/reviewed the merged forward-redaction commit and authored the latest PR-head guard for the scrub rename race. (role: reviewer and adjacent area contributor; confidence: high; commits: a853c5e8c26f, 11bd19ec630f; files: src/config/io.audit.ts, src/config/io.ts, src/config/io.observe-recovery.ts)

Remaining risk / open question:

  • One GitHub Actions check run was still in progress on the latest head at inspection time, so merge should still wait for required CI to finish.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 1ade7a53af1e.

@YB0y
YB0y force-pushed the fix/config-audit-scrub-legacy branch 2 times, most recently from 87d1d25 to 1139a1e Compare May 11, 2026 23:14
@YB0y

YB0y commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Verification: PR #80796 scrubs pre-redactor plaintext-token entries from config-audit.jsonl

PR under test: #80796
PR head SHA: 033ecc944d8af84698e06638988921dc9b945562
Closes issue: #80777

Verdict

Fix verified. PR #80796 correctly scrubs every historical config-audit.jsonl entry that still contains pre-redactor argv tokens by routing each line back through the same redactConfigAuditArgv helper that #75095 introduced for forward writes. Live tokens at rest dropped from 2 → 0 on this box; redacted shapes (xoxb-1…03cT, xapp-1…KEqa) took their place; the scrub is idempotent on a clean log; no other audit-record fields were altered.

Method

Same machine, same shell, same config, only the worktree HEAD differs between phases. Backup of the audit log captured before any test ran. PR was tested end-to-end against the live ~/.openclaw/logs/config-audit.jsonl that issue #80777 was originally filed against — so this is full real-data verification, not a synthetic fixture.

  • Host: Ubuntu 24.04 (Linux 6.8.0-110-generic), Node v22.22.2
  • Pre-test backup: config-audit-BEFORE-2026-05-11T22-57-34Z.jsonl (90727 bytes, 85 entries)
  • Worktree at PR head: /home/orin/Gittensor/OpenCoven/openclaw-upstream @ 033ecc944d (package.json 2026.5.10-beta.1)

Phases:

  1. BEFORE — count plaintext tokens in the live log.
  2. Dry-run previewpnpm openclaw doctor (no --fix) → expect Config audit note flagging N entries, no disk write.
  3. Actual scrubpnpm openclaw doctor --fix → expect Config audit note reporting N scrubbed, disk file rewritten.
  4. AFTER — count plaintext tokens (should be 0); count redacted shapes (should be present).
  5. Idempotencypnpm openclaw doctor again → expect no Config audit panel (nothing left to scrub).

Evidence

Phase counters

Phase Total entries plaintext xoxb- plaintext xapp- redacted xoxb-1… redacted xapp-1… Config audit panel
BEFORE 85 1 1 1 1 — (panel didn't exist)
Dry-run 85 (unchanged) 1 (unchanged) 1 (unchanged) 1 1 2 entries … pre-redactor argv values. Run \openclaw doctor --fix`…`
--fix 86 (+1, doctor's own audit entry) 0 ✓ 0 ✓ 2 2 Scrubbed 2 entries … pre-redactor argv values. Rotate any credentials…
Re-run (idempotency) 86 (unchanged) 0 (unchanged) 0 (unchanged) 2 2 none ✓ — nothing left to scrub

The +1 entry from --fix is the doctor's own config.write audit record for whatever migration it applied during the --fix pass (e.g. removal of agents.defaults.agentRuntime), redacted at write-time by the existing #75095 forward redactor. The +920 bytes is consistent with one new audit entry.

Dry-run preview panel (no --fix)

◇  Config audit ───────────────────────────────────────────────────────────╮
│                                                                          │
│  2 entries in config-audit.jsonl still contain pre-redactor argv values  │
│  (likely plaintext credentials at rest). Run `openclaw doctor --fix` to  │
│  rewrite the argv/execArgv fields through the same redactor used for     │
│  new entries.                                                            │
│                                                                          │
├──────────────────────────────────────────────────────────────────────────╯

File size before/after dry-run: identical (90727 bytes). Plaintext token counts unchanged. Confirms the dry-run path does not touch disk.

Actual scrub panel (--fix)

◇  Config audit ────────────────────────────────────────────────────────╮
│                                                                       │
│  Scrubbed 2 entries in config-audit.jsonl that still contained        │
│  pre-redactor argv values. Rotate any credentials that may have been  │
│  written to the log before the forward redactor shipped.              │
│                                                                       │
├───────────────────────────────────────────────────────────────────────╯

The "Rotate any credentials" sentence is the right operational guidance — even after the at-rest redaction, the tokens were exposed to anyone who could have read the log between pre-#75095 write and the scrub.

Concrete entry transformation

The xoxb- entry from 2026-05-02T00:03:48.471Z (the exact entry called out in issue #80777):

BEFORE (from config-audit-BEFORE-2026-05-11T22-57-34Z.jsonl):

"argv": [
  "/home/orin/.nvm/versions/node/v22.22.2/bin/node",
  "/home/orin/Gittensor/Test/openclaw/openclaw.mjs",
  "config",
  "set",
  "channels.slack.botToken",
  "xoxb-<full-66-char-bot-token-bytes-in-plaintext>"
]

AFTER (live file, post-scrub):

"argv": [
  "/home/orin/.nvm/versions/node/v22.22.2/bin/node",
  "/home/orin/Gittensor/Test/openclaw/openclaw.mjs",
  "config",
  "set",
  "channels.slack.botToken",
  "xoxb-1…03cT"
]

All other record fields (ts, source, event, configPath, pid, ppid, cwd, execArgv, existsBefore, previousHash, nextHash, previousBytes, nextBytes, mode/inode metadata, suspicious, result) are preserved verbatim — forensic continuity intact.

Idempotency

Re-running pnpm openclaw doctor (no --fix) on the already-scrubbed log: no Config audit panel is emitted. The scrubber's same-input/no-rewrite path is correctly skipping already-redacted entries. Counter rewritten returned 0, so the dry-run preview correctly omits the note.

This matches the implementation's contract: redactConfigAuditArgv is a fixed point on already-redacted argv values (it passes *** and xoxb-1…03cT through unchanged), so re-applying the scrubber to a clean log is a no-op.

Safety verification (atomic write, no plaintext leak on failure)

The scrubber writes to a sibling *.scrub.tmp file at mode 0o600, then renames over the audit log; on any error it unlinks the temp file (src/config/io.audit.ts new scrubConfigAuditLog function). I did not crash-inject during this run, but the implementation matches the safety contract claimed in the PR body — a partial scrub cannot leave plaintext at rest because the rename is atomic and the temp file is owner-only-readable from creation.

No .scrub.tmp file is left behind after the successful runs:

$ ls ~/.openclaw/logs/*.scrub* 2>/dev/null || echo "no leftover scrub.tmp files"
no leftover scrub.tmp files

Files saved alongside this verification

  • config-audit-BEFORE-2026-05-11T22-57-34Z.jsonl — backup of the live audit log before any test ran (85 entries, 90727 bytes, mode 0600).
  • config-audit-AFTER-scrub-2026-05-11T23-10-21Z.jsonl — live audit log after the full scrub + idempotent re-run (86 entries, 91647 bytes, mode 0600).
  • doctor-DRYRUN-pr80796-2026-05-11T22-58-52Z.log — full doctor output without --fix, showing the preview panel and no disk write.
  • doctor-FIX-pr80796-2026-05-11T23-06-12Z.log — full doctor output with --fix, showing the scrub panel and the actual disk rewrite.
  • doctor-IDEMPOTENT-pr80796-2026-05-11T23-10-21Z.log — full doctor output on the already-scrubbed log, showing no Config audit panel.

Caveats

  • Verified on Linux. The scrubber implementation is OS-agnostic (uses Node fs.promises with explicit mode: 0o600), but a macOS/Windows live-run wasn't exercised in this verification.
  • The --fix pass also applied at least one other migration during the same run (e.g. agents.defaults.agentRuntime removal — the bug from #80777's sibling issue at qa-reports/05-doctor-changes-past-tense/). That migration added the +1 entry in the AFTER count and is correctly redacted by the forward redactor on its way to disk. Net effect on the scrub verification: nothing — the 2 pre-redactor entries identified by Dry-run were both scrubbed, and the new entry written during --fix was redacted at write-time.
  • The verification was done with the user's live config-audit.jsonl. Slack tokens at rest are now redacted-fingerprints rather than full bytes. Per the scrub panel's own guidance ("Rotate any credentials that may have been written to the log before the forward redactor shipped"), the user should still rotate the affected Slack bot/app tokens since they were exposed at rest in the log window pre-scrub.

@YB0y

YB0y commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

@YB0y
YB0y force-pushed the fix/config-audit-scrub-legacy branch from 1139a1e to 34ca817 Compare May 11, 2026 23:40
@openclaw-barnacle openclaw-barnacle Bot added triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. and removed proof: supplied External PR includes structured after-fix real behavior proof. labels May 11, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 11, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 11, 2026
@YB0y
YB0y force-pushed the fix/config-audit-scrub-legacy branch from 34ca817 to 014d24a Compare May 11, 2026 23:59
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 12, 2026
@sallyom sallyom assigned sallyom and unassigned sallyom May 12, 2026
@YB0y
YB0y force-pushed the fix/config-audit-scrub-legacy branch from 014d24a to d4c7785 Compare May 12, 2026 13:14
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 12, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 12, 2026
YB0y and others added 2 commits May 12, 2026 19:42
…fig-audit.jsonl entries (openclaw#80777)

PR openclaw#75095 added `redactConfigAuditArgv` so newly-written entries in
`~/.openclaw/logs/config-audit.jsonl` mask token-shaped values, but the
audit log has no rotation, retention, or one-shot scrub. Users who ran
`openclaw config set <path> <secret-value>` between commit `748d6821d2`
(audit added 2026-02-14) and `a853c5e8c2` (redactor merged 2026-05-01)
still have plaintext Slack, Telegram, gateway, etc. tokens sitting in
`config-audit.jsonl` at rest after upgrading.

Add `scrubConfigAuditLog` in `src/config/io.audit.ts` that reads the
audit log, rewrites every record's `argv` / `execArgv` fields through
the existing forward redactor, and writes the file back atomically
(`*.scrub.tmp` + rename, mode 0o600). Idempotent — already-masked
entries pass through unchanged. Malformed lines are preserved verbatim
and counted as `skipped` so the scrub never destroys forensic content
it cannot understand. Concurrent-append guard: re-stat the audit log
immediately before rename and abort (no on-disk change) if the file
grew during the scrub, so a parallel appendConfigAuditRecord call is
never overwritten by the rename swap. Wire into the doctor health flow
as `runConfigAuditScrubHealth`: scan-only on a normal `openclaw doctor`
(prints the count and suggests `--fix`); rewrite atomically on
`openclaw doctor --fix` with a "rotate any credentials" note; print a
"stop the gateway and rerun" hint on the abort path. No plaintext
backup file is produced; file mode and parent dir mode are preserved.

Per repo CLAUDE.md ("Legacy config repair: doctor/fix paths, not
startup/load-time core migrations") the scrub is gated to `--fix`
and runs only when the user opts in.

Verified:
- pnpm install --frozen-lockfile
- pnpm test src/config/io.audit.test.ts
- pnpm test src/commands/doctor-config-flow.test.ts
- pnpm tsgo:core:test
- pnpm exec oxfmt --check --threads=1 src/config/io.audit.ts src/config/io.audit.test.ts src/commands/doctor-config-audit-scrub.ts src/flows/doctor-health-contributions.ts

Closes openclaw#80777
@sallyom
sallyom force-pushed the fix/config-audit-scrub-legacy branch from d4c7785 to 11bd19e Compare May 12, 2026 23:42
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 12, 2026
@sallyom

sallyom commented May 12, 2026

Copy link
Copy Markdown
Contributor

Maintainer verification before merge:

  • Rebasing: refreshed origin/main to a0833ed, rebased PR stack cleanly, pushed head 11bd19e with force-with-lease.
  • Local proof: pnpm test src/config/io.audit.test.ts passed, 25 tests.
  • Local proof: pnpm exec oxfmt --check --threads=1 src/config/io.audit.ts src/config/io.audit.test.ts passed.
  • Local proof after final rebase: git diff --check origin/main..HEAD passed.
  • CI on 11bd19e: Real behavior proof, preflight, security-dependency-audit, check-lint, check-prod-types, check-test-types, build-artifacts, build-smoke, config-boundary, and the selected node/check shards passed. The aggregate CodeQL entry is neutral while the individual Security High CodeQL jobs passed.

Maintainer-assisted follow-up commit adds the final stat guard after writing the temp scrub file, so a config-audit append between temp write and rename aborts without overwriting the live log.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 12, 2026
@sallyom
sallyom merged commit 14a9bb9 into openclaw:main May 12, 2026
114 of 116 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: M

Projects

None yet

2 participants