You are a ProClaw developer assigned to WS-B: Credential Scrubber + Config Integrity Protection.

  **Priority:** P0 — Launch 1
  **Write access:** `runner/` only
  **Read-only:** `contracts/`, `lib/`, `cli/`, `interceptor/`

  ## CONTEXT

  ProClaw is an AI agent runner. When the agent calls tools (shell_exec, web_fetch, file_read), the tool output goes into the LLM conversation messages[]. If a tool output accidentally contains an API key
  (e.g., the agent reads ~/.bashrc and finds DEEPSEEK_API_KEY=sk-abc123), that key enters the LLM context and could leak via the model's response.

  Additionally, the user's config file (~/.proclaw/config.json) stores their API key. A corrupted write (crash mid-write, invalid JSON) can destroy the config and lock the user out.

  You will create TWO features:

  ## DELIVERABLE 1: Credential Scrubber (~200-250 LOC)

  ### Create `runner/credential_scrubber.py`

  ```python
  """Scrubs credentials from tool outputs before they enter the LLM context."""
  from __future__ import annotations

  import re
  import logging
  from typing import Any

  logger = logging.getLogger("proclaw.runner.credential_scrubber")

  # Patterns that look like API keys or secrets
  CREDENTIAL_PATTERNS: list[tuple[str, re.Pattern]] = [
      ("Generic API Key", re.compile(r"""(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token|bearer)\s*[=:]\s*['"]?([A-Za-z0-9_\-]{20,})['"]?""", re.IGNORECASE)),
      ("OpenAI Key", re.compile(r"sk-[A-Za-z0-9]{20,}")),
      ("Anthropic Key", re.compile(r"sk-ant-[A-Za-z0-9_\-]{20,}")),
      ("DeepSeek Key", re.compile(r"sk-[a-f0-9]{32,}")),
      ("MiniMax Key", re.compile(r"eyJhbG[A-Za-z0-9._-]{20,}")),
      ("AWS Key", re.compile(r"AKIA[0-9A-Z]{16}")),
      ("GitHub Token", re.compile(r"gh[pousr]_[A-Za-z0-9_]{36,}")),
      ("Generic Bearer", re.compile(r"Bearer\s+[A-Za-z0-9_\-\.]{20,}")),
      ("Base64 Long Secret", re.compile(r"""(?:password|passwd|secret|token|credential)\s*[=:]\s*['"]?([A-Za-z0-9+/=]{40,})['"]?""", re.IGNORECASE)),
  ]

  REDACTION_MARKER = "[REDACTED-CREDENTIAL]"


  class CredentialScrubber:
      """Scrubs detected credentials from text before it enters the LLM context.

      Also accepts explicit secrets (like the user's configured API key)
      which are always scrubbed regardless of pattern matching.
      """

      def __init__(self, extra_secrets: list[str] | None = None):
          """
          Args:
              extra_secrets: Explicit secret strings to always scrub (e.g., the
                  user's API key from RunnerConfig.api_key). These are matched
                  by exact substring, not regex.
          """
          self._extra_secrets: list[str] = [s for s in (extra_secrets or []) if s and len(s) >= 8]
          self._stats = {"scanned": 0, "redacted": 0, "pattern_hits": {}}

      def scrub(self, text: str) -> tuple[str, int]:
          """Scrub credentials from text.

          Returns:
              Tuple of (scrubbed_text, redaction_count).
          """
          ...  # implement: first scrub extra_secrets by exact match, then run CREDENTIAL_PATTERNS

      def scrub_tool_result(self, output: str) -> str:
          """Convenience: scrub a tool result output string.

          Logs warnings when credentials are found.
          """
          ...

      @property
      def stats(self) -> dict:
          """Return scrubbing statistics."""
          return dict(self._stats)

  Wire into runner/agent_loop.py

  The scrubber runs on every tool result BEFORE the output is appended to messages[]:

  # In _process_tool_call(), after tool.execute() returns result:
  if self._credential_scrubber is not None:
      result = ToolResult(
          success=result.success,
          output=self._credential_scrubber.scrub_tool_result(result.output),
          error=result.error,
          truncated=result.truncated,
      )

  Wire into runner/entrypoint.py

  # After resolving config, build scrubber
  credential_scrubber = None
  extra_secrets = []
  if config.api_key:
      extra_secrets.append(config.api_key)
  # Also scrub any key from environment
  import os
  for env_var in ["DEEPSEEK_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MINIMAX_API_KEY"]:
      val = os.environ.get(env_var, "")
      if val:
          extra_secrets.append(val)

  if extra_secrets:
      from runner.credential_scrubber import CredentialScrubber
      credential_scrubber = CredentialScrubber(extra_secrets=extra_secrets)
      logger.info("Credential scrubber: enabled (%d explicit secrets)", len(extra_secrets))

  Pass credential_scrubber to AgentRunner.__init__() and store as self._credential_scrubber.

  Add credential_scrubber parameter to AgentRunner.__init__() in agent_loop.py

  Add it as an optional parameter (default None) just like context_manager, tool_cache, retry_policy.

  DELIVERABLE 2: Config Integrity Protection (~100-150 LOC)

  Create runner/config_guard.py

  """Atomic config file operations with validation and backup."""
  from __future__ import annotations

  import json
  import logging
  import os
  import shutil
  import tempfile
  from pathlib import Path
  from typing import Any

  logger = logging.getLogger("proclaw.runner.config_guard")


  class ConfigGuard:
      """Protects ~/.proclaw/config.json from corruption.

      - Atomic writes: write to temp file -> validate JSON -> rename
      - Schema validation: ensure required fields present after write
      - Backup: save previous config as config.json.bak before overwrite
      """

      REQUIRED_FIELDS = {"provider", "model"}  # minimum viable config

      def __init__(self, config_path: str | Path):
          self._path = Path(config_path)

      def safe_write(self, data: dict[str, Any]) -> None:
          """Write config atomically with validation and backup.

          Steps:
          1. Serialize to JSON string
          2. Validate JSON is parseable (round-trip)
          3. Validate required fields present
          4. If existing config exists, copy to .bak
          5. Write to temp file in same directory
          6. Rename temp -> config.json (atomic on POSIX, near-atomic on Windows)
          """
          ...

      def safe_read(self) -> dict[str, Any]:
          """Read config with fallback to backup.

          If config.json is corrupted/missing, try config.json.bak.
          """
          ...

      def validate(self, data: dict[str, Any]) -> list[str]:
          """Validate config data. Returns list of errors (empty = valid)."""
          ...

  Wire into cli/config.py (READ-ONLY for you — just describe the integration point)

  The ConfigGuard should be used wherever save_config() is called in cli/config.py. Since you don't have write access to cli/, document the integration in a docstring:

  # In runner/config_guard.py docstring:
  """
  Integration with cli/config.py:
      Replace: json.dump(data, f, indent=2)
      With:    ConfigGuard(config_path).safe_write(data)

      Replace: json.load(f)
      With:    ConfigGuard(config_path).safe_read()
  """

  TESTS REQUIRED

  runner/tests/test_credential_scrubber.py

  1. No credentials → text unchanged
  2. OpenAI key pattern → redacted
  3. Anthropic key pattern → redacted
  4. DeepSeek key pattern → redacted
  5. MiniMax key pattern → redacted
  6. AWS key pattern → redacted
  7. GitHub token pattern → redacted
  8. Generic API key in env file format → redacted
  9. Bearer token → redacted
  10. Extra secret (user's API key) → exact match redacted
  11. Short strings (<8 chars) in extra_secrets → ignored (not a real key)
  12. Multiple credentials in one text → all redacted, count correct
  13. Credential in JSON output → redacted within JSON
  14. stats property tracks redaction counts

  runner/tests/test_config_guard.py

  1. safe_write() → file contains valid JSON
  2. safe_write() → backup created (.bak)
  3. safe_write() with missing required field → raises ValueError
  4. safe_write() atomic: if validation fails, original file unchanged
  5. safe_read() → returns parsed config
  6. safe_read() with corrupted file → falls back to .bak
  7. safe_read() with no file and no .bak → raises FileNotFoundError
  8. validate() with valid data → empty error list
  9. validate() with missing field → error list contains field name

  FILES YOU'LL CREATE/MODIFY

  ┌──────────────────────────────────────────┬─────────────────────────────────────────────────────────────┐
  │                   File                   │                           Action                            │
  ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
  │ runner/credential_scrubber.py            │ CREATE                                                      │
  ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
  │ runner/config_guard.py                   │ CREATE                                                      │
  ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
  │ runner/agent_loop.py                     │ MODIFY — Add credential_scrubber parameter + wire scrubbing │
  ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
  │ runner/entrypoint.py                     │ MODIFY — Build and inject credential scrubber               │
  ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
  │ runner/tests/test_credential_scrubber.py │ CREATE                                                      │
  ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
  │ runner/tests/test_config_guard.py        │ CREATE                                                      │
  └──────────────────────────────────────────┴─────────────────────────────────────────────────────────────┘

  EXISTING CODE YOU NEED TO READ FIRST

  Read these files to understand current interfaces:
  - runner/models.py — ToolResult, RunnerConfig
  - runner/agent_loop.py — AgentRunner class, _process_tool_call() method
  - runner/entrypoint.py — run_agent() wiring, build_tool_registry(), build_bridge()
  - runner/tool_protocol.py — Tool Protocol, ToolRegistry

  CRITICAL RULES

  1. All features ADDITIVE and OPTIONAL. When no credential_scrubber is configured, tool outputs pass through unchanged (current behavior).
  2. Do NOT modify files outside runner/. cli/config.py is READ-ONLY — document integration points only.
  3. Run existing tests after your changes: pytest runner/tests/ -v — all must pass.
  4. Run your new tests: pytest runner/tests/test_credential_scrubber.py runner/tests/test_config_guard.py -v
  5. The credential scrubber ALWAYS runs when configured — it's not optional per-tool. Every tool output gets scrubbed.
  6. Extra secrets (user's API key) must be scrubbed by EXACT substring match, not pattern. If the user's key is "sk-abc123xyz", that exact string must be replaced even if no regex matches it.