{"slug": "tool-stdout-is-a-secret-envelope", "title": "Tool Stdout Is a Secret Envelope", "summary": "A developer warns that tool stdout is a secret envelope, arguing that command output, test logs, and stack traces can leak sensitive data to remote coding assistants. They propose a local redaction gate that filters high-risk patterns before any tool output is sent to a model, treating all tool results as untrusted data. The article includes a Python script as a proposed preflight filter and notes that MonkeyCode offers free model access and a free server option, with a disclosure that the piece is part of product outreach.", "body_md": "When you wire a coding assistant to your shell, the dangerous payload is rarely the prompt you typed. Secrets ride out in command output, test logs, and stack traces that tools dump without asking. You should treat every tool result as untrusted data that must cross a redaction gate before it reaches a remote model. That gate is a local program you control, not a hopeful instruction buried in a system prompt.\n\nPicture the coding assistant as a contractor who stands just outside your office glass all afternoon. You can hand papers through a slot, yet you cannot unsay anything once the page leaves the room. Remote inference works the same way, because the model and any request store sit beyond your process boundary. A free server does not change that geometry; it only makes the slot easier to use during ordinary weekday work.\n\nThe threat model stays small if you draw it on one page and refuse to romanticize agents. Your laptop still holds secrets, customer fixtures, and deployment wiring that must never travel with a prompt. The remote model is an untrusted peer that may train on nothing and still keep logs you never see. Between those two rooms sits the agent loop, which will happily cat a file because a stack trace named its path.\n\nYou already know not to paste a production env file into a chat box like a tourist. The quieter failure is `docker compose config`, `kubectl describe`, `env`, and a failing test that prints a signed URL. Those commands are useful, so you run them, and the tool layer forwards the stdout as if it were just another comment. The model then helps by quoting the secret back, which copies it into yet another persisted log line.\n\nIf you practice this workflow against a remote coding assistant, keep the product in its place as a convenience, not as a vault. MonkeyCode offers free model access and a free server option that can host that loop while you iterate on local gates. Disclosure: This article was prepared as part of MonkeyCode's product outreach, stated here before any workflow advice continues. The disclosure does not change the boundary, because the server still sits outside your laptop, so redaction stays local.\n\nDraw four boundaries even if your diagram looks boring on a whiteboard in a quiet hallway. One boundary is the agent process versus the files it can open without extra confirmation from you. Another is the network cable, where a completion request leaves and you lose practical recall of the bytes. Persistence and people complete the map, because transcripts and operators may outlive the debugging session you meant to keep brief.\n\nSecrets are not only API keys sitting in a file named env, though that file remains a classic mistake. They also hide in PEM blocks, bearer headers, database URLs, session cookies, and cloud credential process exports. Customer fixtures can be worse, because a unique email in a failing test can identify a real person. You should assume any string that grants access or names a private person is ineligible for the model.\n\nHope is not a control, so you want a local gate that fails closed when it sees a high risk pattern. The script below is a proposed filter you run on your machine before any tool stdout is attached to a request. It refuses known credential shapes and sensitive path names, then writes a sanitized bundle or exits with an error. Treat it as a seatbelt rather than a vault door, because regular expressions miss encodings and novel token formats.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"Proposed local preflight gate for assistant context. Run this on your machine.\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\nDENY_NAME_PARTS = (\n    '.env',\n    'id_rsa',\n    'id_ed25519',\n    'credentials',\n    'kubeconfig',\n    'serviceaccount',\n    '.pem',\n    '.p12',\n    'terraform.tfstate',\n)\n\nPATTERNS = (\n    ('aws_access_key', re.compile(r'AKIA[0-9A-Z]{16}')),\n    ('pem_block', re.compile(r'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----')),\n    ('bearer', re.compile(r'(?i)bearer\\s+[A-Za-z0-9._\\-]+=*')),\n    ('jwt', re.compile(r'eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}')),\n    ('db_url', re.compile(r'(?i)(?:postgres|mysql|mongodb|redis)://[^\\s]+')),\n    ('generic_secret_assign', re.compile(r'(?i)(api[_-]?key|secret|password|token)\\s*[:=]\\s*\\S+')),\n)\n\nPLACEHOLDER = '[REDACTED:{label}]'\n\ndef path_denied(path: Path) -> str | None:\n    lowered = str(path).lower()\n    for part in DENY_NAME_PARTS:\n        if part in lowered:\n            return part\n    return None\n\ndef scan_text(text: str) -> list[str]:\n    hits = []\n    for label, cre in PATTERNS:\n        if cre.search(text):\n            hits.append(label)\n    return hits\n\ndef redact_text(text: str) -> str:\n    out = text\n    for label, cre in PATTERNS:\n        out = cre.sub(PLACEHOLDER.format(label=label), out)\n    return out\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(description='Fail closed on secret-shaped context.')\n    parser.add_argument('paths', nargs='*', type=Path)\n    parser.add_argument('--stdin', action='store_true')\n    parser.add_argument('--redact', action='store_true', help='Print redacted text instead of refusing.')\n    parser.add_argument('--max-bytes', type=int, default=200000)\n    args = parser.parse_args()\n\n    chunks: list[tuple[str, str]] = []\n    if args.stdin or not args.paths:\n        data = sys.stdin.read(args.max_bytes + 1)\n        if len(data) > args.max_bytes:\n            print('gate: stdin exceeds max-bytes', file=sys.stderr)\n            return 2\n        chunks.append(('<stdin>', data))\n\n    for path in args.paths:\n        if not path.exists():\n            print(f'gate: missing path {path}', file=sys.stderr)\n            return 2\n        denied = path_denied(path)\n        if denied:\n            print(f'gate: refuse path {path} (matched {denied})', file=sys.stderr)\n            return 2\n        data = path.read_text(encoding='utf-8', errors='replace')\n        if len(data.encode('utf-8')) > args.max_bytes:\n            print(f'gate: {path} exceeds max-bytes', file=sys.stderr)\n            return 2\n        chunks.append((str(path), data))\n\n    failed = False\n    for name, text in chunks:\n        hits = scan_text(text)\n        if not hits:\n            if args.redact:\n                sys.stdout.write(text)\n            continue\n        failed = True\n        print(f'gate: {name} hit {\",\".join(hits)}', file=sys.stderr)\n        if args.redact:\n            sys.stdout.write(redact_text(text))\n    if failed and not args.redact:\n        return 2\n    return 0\n\nif __name__ == '__main__':\n    raise SystemExit(main())\n```\n\nYou can wrap a noisy command so the raw bytes never reach the clipboard or the assistant tool channel. The shell function below runs the command, stores stdout in a temp file, and only prints the gated result. If the gate exits nonzero, you keep the original output local and send a short note that redaction refused the payload. That refusal is the success case, not an inconvenience, because a blocked secret never becomes a completion prompt.\n\n```\n# Proposed wrapper. Keep raw output out of the assistant tool path.\ngate_run() {\n  raw=\"$(mktemp \"${TMPDIR:-/tmp}/gate.XXXXXX\")\"\n  if ! \"$@\" >\"$raw\" 2>&1; then\n    echo \"gate_run: command failed; output kept in $raw\" >&2\n  fi\n  if python3 gate_context.py --stdin <\"$raw\"; then\n    cat \"$raw\"\n    rm -f \"$raw\"\n    return 0\n  fi\n  echo \"gate_run: refused to forward stdout; inspect $raw locally\" >&2\n  return 2\n}\n\n# Synthetic checks only. Do not pipe live production secrets into a model to \"see what happens\".\n# gate_run printf 'token=not-a-real-secret\\n'\n# printf '-----BEGIN PRIVATE KEY-----\\nMIIFAKE\\n' | python3 gate_context.py --stdin; echo exit:$?\n```\n\nRun the same gate against files the agent wants to attach, not only against live command output from your terminal. A typical preflight looks like listing candidate paths, grepping for secret-looking names, and then handing survivors to the filter. You should still read the sanitized bundle, because a false negative looks like ordinary application text until someone replays the log. Keep the original transcript in a local directory that your assistant allowlist cannot read, and rotate it like any other secret store.\n\n```\n# Proposed name scan, then content scan. Do not point this at production secret stores.\nfind . -type f \\( -name '.env*' -o -name '*credential*' -o -name 'id_rsa*' \\) -print\npython3 gate_context.py app.log testdata/sample_trace.txt\n```\n\nAgentic loops make this worse because the model can request another tool call after you thought the context was clean. If the error message still contains a relative path, the next call may open the unsanitized file and skip your wrapper. You close that hole by making the only legal tool a wrapper that always runs the gate, never a raw cat. Think of it as a mail room: every envelope goes through the scanner, including replies that look like innocent follow-up questions.\n\nA proposed local test is enough to catch regressions when you add a new secret pattern next month. Put a fake AWS-style token and a tiny PEM block in a fixture file that never leaves your repository's testdata folder. Assert that the gate exits nonzero on the raw fixture and exits zero after you replace those blocks with placeholders. Label this unexecuted until you run it; the value is the habit of testing the control, not a published benchmark.\n\n```\n# proposed_test_gate.py — unexecuted proposal, not a measured result\nfrom pathlib import Path\nimport subprocess\nimport sys\n\ndef test_refuses_pem_and_akia(tmp_path: Path) -> None:\n    fixture = tmp_path / 'trace.txt'\n    fixture.write_text(\n        '-----BEGIN PRIVATE KEY-----\\nMIIFAKE\\n-----END PRIVATE KEY-----\\n'\n        'AWS_ACCESS_KEY_ID=AKIAIOSAMPLEEXAMPLE\\n',\n        encoding='utf-8',\n    )\n    proc = subprocess.run(\n        [sys.executable, 'gate_context.py', str(fixture)],\n        check=False,\n    )\n    assert proc.returncode != 0\n```\n\nThis approach will not save you if the secret is split across chunks, base64 wrapped, or rendered inside a screenshot. It also will not satisfy a regulator who forbids sending customer content to any third party inference provider at all. Teams handling health records, cardholder data, or classified material should not use a remote coding model for those files. People who cannot run the gate on their own laptop should not paste production output into a browser chat either.\n\nThe contractor outside the glass can still help you rename a function, once the envelope contains only what you meant to share. Keep the threat model on one page, and keep raw stdout in a directory the agent cannot list. You will still ship bugs, yet you will ship fewer accidental keys, which is the incident most teams can actually prevent. That is enough of a win to keep the mail room boring, which is exactly how a trust boundary is supposed to feel.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.", "url": "https://wpnews.pro/news/tool-stdout-is-a-secret-envelope", "canonical_source": "https://dev.to/devrs_886/tool-stdout-is-a-secret-envelope-2pi6", "published_at": "2026-09-09 16:21:01+00:00", "updated_at": "2026-09-09 16:48:48.189014+00:00", "lang": "en", "topics": ["ai-safety", "developer-tools", "ai-agents"], "entities": ["MonkeyCode"], "alternates": {"html": "https://wpnews.pro/news/tool-stdout-is-a-secret-envelope", "markdown": "https://wpnews.pro/news/tool-stdout-is-a-secret-envelope.md", "text": "https://wpnews.pro/news/tool-stdout-is-a-secret-envelope.txt", "jsonld": "https://wpnews.pro/news/tool-stdout-is-a-secret-envelope.jsonld"}}