{"slug": "a-proxy-that-scans-llm-requests-attach-for-secrets-before-they-reach-the-model", "title": "A proxy that scans LLM requests/attach for secrets before they reach the model", "summary": "Geoff Huntley released preflight, a local proxy that scans OpenAI-compatible LLM requests and their attachments for secrets before they reach the model, running on 127.0.0.1:8081 in front of the underclass router on 127.0.0.1:8080. Preflight redacts detected credentials with [REDACTED:rule-id] placeholders by default, decodes images and PDFs locally for text, metadata, barcode and OCR inspection, and caches identical attachment inspections to avoid repeated OCR work. The tool parses /v1/responses, /v1/chat/completions and /v1/models traffic, applies a single request-wide decision to redact, rebuild or reject, and forwards only approved bytes, with installation via `nix run github:ghuntley/preflight -- serve`.", "body_md": "**A local proxy that scans LLM requests and attachments for secrets before they reach the model.**\n\n```\n                       ┌────────────────────────────────────┐\n                       │             preflight              │\n                       │                                    │\n coding harness ──────►│  /v1/responses                     │\n (any supported        │  /v1/chat/completions              │────► underclass ────► model\n  OpenAI-compatible    │  /v1/models                        │\n  client)              │                                    │\n                       │  decode · inspect · redact/block   │\n                       │  local OCR · content-addressed     │\n                       │  cache · structured logging        │\n                       └────────────────────────────────────┘\n                             127.0.0.1:8081                     127.0.0.1:8080\n```\n\nSomeone pastes an `.env` file? Detected credentials become stable placeholders and the request continues. A screenshot or PDF contains a key? preflight decodes it, runs local extraction and OCR, and sanitizes it where supported. When inspection or safe rewriting is impossible, the request stays grounded.\n\nCoding agents read source files, shell output, screenshots, and documents. Secrets can arrive through any of them. preflight sits between the harness and [underclass](https://github.com/ghuntley/underclass), inspecting the outbound copy before inference starts:\n\n- **One base URL change** — keep the supported OpenAI HTTP endpoints, tool-call structure, session headers, and response streams.\n- **Redact by default** — replace detected secrets with`[REDACTED:rule-id]` so ordinary pasted-key incidents do not stop the agent.\n- **Attachments get inspected too** — decode images and PDFs locally; scan extracted text, metadata, barcodes, and OCR output.\n- **No repeated OCR tax** — identical attachments reuse completed inspection results while their scope and inspection profile remain the same.\n- **No generic randomness detector** — prompts and source dumps are entropy soup. Default rules use recognizable credential shapes.\n- **Nothing forwarded halfway through inspection** — the whole request is checked before it goes to underclass.\n\nWith underclass running on `127.0.0.1:8080`, start preflight with its complete document-processing toolchain:\n\n```\nnix run github:ghuntley/preflight -- serve\n```\n\nPoint the harness at:\n\n```\nhttp://127.0.0.1:8081/v1\n```\n\nKeep using the existing underclass API key. By default, preflight passes authorization through. You can also configure separate client-facing and upstream credentials.\n\nFor a local checkout:\n\n```\ndevenv shell -- cargo build --locked --bins\ndevenv shell -- cargo run --locked --bin preflight -- serve\n```\n\nBuilding all binaries also builds the Rust attachment worker. Startup checks the sandboxed native toolchain before the proxy becomes ready.\n\n- **Parse first.** Walk decoded JSON string leaves, including messages, instructions, tool results, and nested JSON tool arguments. Duplicate object keys are rejected.\n- **Match structured content.** Each text unit goes through keyword filtering, a Gitleaks-derived regex, optional entropy filtering, and the trusted allowlist. Ordered text parts and wrapped JWTs get mapped reconstruction passes.\n- **Inspect attachments locally.** Resolve their bytes, decode them, extract text, render PDF pages, and run OCR. External attachment URLs never receive the underclass credential.\n- **Apply one request-wide decision.** Redact supported spans, rebuild affected artifacts, or reject the request. Rebuilt attachments are inspected again before approval.\n- **Forward approved content.** Enforcing modes send the exact approved bytes or sanitized replacement. Unchanged clean requests preserve their original body bytes; underclass's response streams through.\n\nPreflight does not retry inference requests. Underclass owns provider routing and failover.\n\nDesign decisions and their trade-offs live in [`docs/adr/`](https://github.com/ghuntley/preflight/blob/main/docs/adr) — start with [ADR 0001](https://github.com/ghuntley/preflight/blob/main/docs/adr/0001-inspection-transaction.md) for the inspection transaction and [ADR 0002](https://github.com/ghuntley/preflight/blob/main/docs/adr/0002-rust-workers-and-content-cache.md) for workers and caching.\n\n| mode | what happens | \n|---|---|\n| `redact` | Default. Replace text findings and sanitize supported attachments. Forward the rewritten request; return HTTP 409 when a finding has no safe replacement. | \n| `no-go` | Return HTTP 409 for any non-allowlisted finding. Nothing reaches underclass. The response contains opaque finding IDs, never the secret. | \n| `advisory` | Report findings and forward the original content. Useful for tuning; detected secrets can reach the model in this mode. | \n\nAll modes reject acquisition or extraction failures. Inspection failures return `422`, malformed JSON `400`, body limits `413`, and deadline exhaustion `408`. Preflight-generated errors use static codes rather than matched content.\n\nOptional TOML configuration, supplied explicitly:\n\n```\npreflight serve --config /path/to/config.toml\n```\n\n| key | default | meaning | \n|---|---|---|\n| `bind` | `127.0.0.1:8081` | listen address | \n| `upstream` | `http://127.0.0.1:8080` | underclass base URL, without `/v1` | \n| `mode` | `redact` | enforcement policy | \n| `sandbox` | `true` | isolate attachment workers with Bubblewrap | \n| `allow_page_redaction` | `false` | permit blacking out a PDF page when a finding cannot be mapped to an OCR region | \n| `max_body_bytes` | `67108864` | maximum request body size: 64 MiB | \n| `request_timeout_secs` | `180` | deadline covering inspection and waiting for upstream response headers | \n| `carnet` | unset | path to a JSON array of exact secret SHA-256 hashes | \n| `stopwords` | empty | exact whole-secret exemptions | \n| `upstream_entropy` | `false` | apply upstream entropy thresholds to enabled rules | \n| `cache_dir` | `$XDG_CACHE_HOME/preflight` or`~/.cache/preflight` | persistent verdicts and sanitized artifacts | \n| `control_socket` | `/tmp/preflight-control.sock` | local cache administration socket | \n| `resolver.file_api_base` | unset | trusted OpenAI-compatible file API base, such as `https://api.openai.com/v1` | \n\nExample:\n\n```\nbind = \"127.0.0.1:8081\"\nupstream = \"http://127.0.0.1:8080\"\nmode = \"redact\"\ncarnet = \"/run/secrets/preflight-carnet.json\"\n\n[cache]\nmemory_entries = 50000\ndisk_entries = 100000\nartifact_bytes = 1073741824\nmax_artifact_bytes = 67108864\nttl_secs = 604800\n```\n\nRuntime environment:\n\n| variable | purpose | \n|---|---|\n| `PREFLIGHT_CONFIG` | configuration path for `serve` | \n| `PREFLIGHT_CLIENT_KEY` | optional bearer credential required from clients | \n| `PREFLIGHT_UPSTREAM_KEY` | optional replacement credential sent to underclass | \n| `PREFLIGHT_FILE_API_KEY` | separate credential for the configured file API | \n| `PREFLIGHT_CONTROL` | socket path for cache administration commands | \n| `RUST_LOG` | logging filter; output is structured JSON | \n\nSend SIGHUP to reload configuration and the carnet. A failed reload keeps the previous runtime active, and admitted requests retain their original snapshots. Listener, cache directory/budgets, and control-socket changes require a restart. On NixOS: `systemctl reload preflight`.\n\n```\npreflight serve [--config PATH]\npreflight check [--config PATH]\npreflight cache status [--socket PATH]\npreflight cache purge [--scope SCOPE_ID] [--socket PATH]\n```\n\n`check` validates configuration and compiles the detection rules. Cache commands talk to the running daemon through a mode-`0600` Unix socket. On NixOS, run administration as root.\n\n| route | auth | purpose | \n|---|---|---|\n| `POST /v1/responses` | client key, if configured | inspect a Responses request, then stream through underclass | \n| `POST /v1/chat/completions` | client key, if configured | inspect a Chat Completions request, then stream through underclass | \n| `GET /v1/models` | client key, if configured | forward model discovery | \n| `GET /healthz` | none | local liveness probe | \n| `GET /readyz` | none | readiness after startup toolchain checks | \n| `GET /metrics` | none | aggregate Prometheus counters and request-to-headers timings | \n\nInference responses carry `x-preflight-request-id`. Completed inspections also attach `x-preflight-finding-count`. JSON logs correlate requests and safe finding IDs without recording prompts or matched secrets. Unknown routes are not pass-through routes.\n\nPreflight prints one JSON object per log line. These representative excerpts omit tracing's `span` and `spans` metadata for readability; in the full output, inspection events carry the request ID and inspection-profile fingerprint in their request span. Timestamps, IDs, and timings below are illustrative.\n\nThe headings identify the configured policy. The current log schema does **not** include a `mode` or `action` field, and HTTP 200 alone does not distinguish redaction from advisory forwarding. Successful-forwarding examples assume underclass returns 200.\n\nInspection completes with zero findings, and the request continues normally:\n\n```\n{\"timestamp\":\"2026-09-20T12:00:00.001Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"inspection.completed\",\"finding_count\":0}}\n{\"timestamp\":\"2026-09-20T12:00:00.024Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"request.policy_completed\",\"request_id\":\"9031b620-66aa-4a59-9228-bb7f40f567a1\",\"status\":200,\"duration_ms\":24}}\n```\n\nThere is no `inspection.finding` event for this request. An allowlisted fixture also contributes no finding.\n\nA text finding produces a warning with its rule and opaque finding ID. Preflight replaces the detected span with a token such as `[REDACTED:github-pat]`, then forwards the sanitized request:\n\n```\n{\"timestamp\":\"2026-09-20T12:01:00.002Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"inspection.completed\",\"finding_count\":1}}\n{\"timestamp\":\"2026-09-20T12:01:00.002Z\",\"level\":\"WARN\",\"fields\":{\"event\":\"inspection.finding\",\"finding_id\":\"8a4f0571-4751-4d64-94da-55c3626a12de\",\"rule_id\":\"github-pat\"}}\n{\"timestamp\":\"2026-09-20T12:01:00.031Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"request.policy_completed\",\"request_id\":\"aa7445a8-b378-4b93-8ed1-b25ae80dfc01\",\"status\":200,\"duration_ms\":31}}\n```\n\nFor rebuilt attachments, an additional `attachment.inspected` event includes `finding_count` and `rebuilt: true`. A finding that cannot be safely rewritten is blocked instead.\n\nThe finding is reported, then preflight returns 409 without sending the request to underclass:\n\n```\n{\"timestamp\":\"2026-09-20T12:02:00.002Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"inspection.completed\",\"finding_count\":1}}\n{\"timestamp\":\"2026-09-20T12:02:00.002Z\",\"level\":\"WARN\",\"fields\":{\"event\":\"inspection.finding\",\"finding_id\":\"4b23f164-cc68-4852-b34d-1c6bd0ee13bd\",\"rule_id\":\"github-pat\"}}\n{\"timestamp\":\"2026-09-20T12:02:00.003Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"request.policy_completed\",\"request_id\":\"9bf853df-9d39-4f05-a189-a857de29596a\",\"status\":409,\"duration_ms\":3}}\n```\n\nThe client error contains the same finding ID, letting the operator locate the rule in logs. Neither the log nor the error contains the secret. The completion event stays at `INFO`; the finding itself is `WARN`.\n\nThe finding is reported, but the original request is forwarded unchanged:\n\n```\n{\"timestamp\":\"2026-09-20T12:03:00.002Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"inspection.completed\",\"finding_count\":1}}\n{\"timestamp\":\"2026-09-20T12:03:00.002Z\",\"level\":\"WARN\",\"fields\":{\"event\":\"inspection.finding\",\"finding_id\":\"c5aebff1-7b5a-4ad1-9ae7-2c351b96106f\",\"rule_id\":\"github-pat\"}}\n{\"timestamp\":\"2026-09-20T12:03:00.028Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"request.policy_completed\",\"request_id\":\"ce36b020-0321-41e4-8d27-a9d81999a91e\",\"status\":200,\"duration_ms\":28}}\n```\n\nThe model can receive the detected secret in this mode. Reporting remains secret-free.\n\nA decoding or extraction failure is different from a clean scan. For example:\n\n```\n{\"timestamp\":\"2026-09-20T12:04:00.015Z\",\"level\":\"INFO\",\"fields\":{\"event\":\"request.policy_completed\",\"request_id\":\"2dc4f68c-adfd-4886-8965-708dabd14374\",\"status\":422,\"duration_ms\":15}}\n```\n\nThere is no successful request-wide `inspection.completed` event in this case. All modes reject incomplete inspection. `request.policy_completed.duration_ms` measures time through the response headers; a forwarded response later emits `response.completed`, `response.stream_failed`, or `response.cancelled` for its stream outcome.\n\nThe Gitleaks database is embedded in the binary. The vendored snapshot includes its license and a release/commit/checksum manifest in [`vendor/gitleaks/`](https://github.com/ghuntley/preflight/blob/main/vendor/gitleaks). [` rules/default-profile.toml`](https://github.com/ghuntley/preflight/blob/main/rules/default-profile.toml) explicitly selects the enabled upstream rules, so new upstream additions do not silently expand enforcement.\n\nDefaults cover AWS access-key IDs, GitHub PATs, OpenAI, Anthropic, Google/Gemini, OpenRouter, Slack, Stripe, private keys, and JWTs. Generic entropy heuristics stay disabled. Strong prefix rules use no entropy suppression unless `upstream_entropy=true` is configured.\n\n**The allowlist is a carnet: exact hashes and stopwords.** A carnet entry exempts the SHA-256 hash of an exact decoded secret value. Stopwords match whole secrets, not surrounding prose or substrings. Upstream path exclusions, broad regex allowlists, and inline `gitleaks:allow` comments do not grant exemptions.\n\nProperty-name and control-field findings are rejected in enforcing modes when rewriting them would change request semantics.\n\nAll project-owned executables are Rust. The worker uses typed subprocess adapters for Poppler, QPDF, Tesseract, ExifTool, and ZBar; Rust image codecs handle JPEG/PNG and `lopdf` constructs replacement PDFs. Nix pins the toolchain.\n\n| content | inspection | \n|---|---|\n| JPEG / PNG | pixel OCR, barcode decoding, and metadata | \n| APNG | frame inspection; multi-frame sanitization currently fails closed | \n|  | extracted text, QPDF object strings, metadata, original embedded images, and rendered-page OCR | \n| Embedded PDF attachments | recursive extraction within depth, count, and byte limits | \n| UTF-8 / BOM-marked UTF-16 text | decode and scan text directly | \n| Base64 / data URLs | decode the transport representation before inspection | \n| Remote HTTPS URLs | bounded, public-address-only, DNS-pinned retrieval with no redirects; inline the inspected bytes in enforcing modes | \n| Provider file IDs | retrieve through the configured OpenAI-compatible `/files/{id}/content` adapter | \n\nOCR includes quarter-turn orientations and a reduced-resolution pass for large images. Coordinate maps bring findings back to original pixels. Scanning original embedded images as well as rendered PDF pages catches details that PDF resampling can erase.\n\nImages are re-encoded after opaque pixel redaction, without original metadata. PDF replacements contain only rendered raster pages. `allow_page_redaction=true` permits removing an entire affected page's visual content when precise mapping is unavailable; otherwise the request is rejected. PDF reconstruction is lossy, and OCR does not prove the absence of every visually readable secret.\n\nUnsupported PDF active content, optional layers, encryption, and incremental revisions fail closed. Annotations and forms exposed through object strings are inspected. Other file protocols need explicit adapters.\n\n**Same bytes, same scope, same inspection profile: reuse the result.** Memory verdicts are backed by SQLite, with sanitized artifacts stored as separate private files.\n\n- **Content-addressed.** Keys bind exact bytes, credential-derived scope, rules/carnet, sanitization settings, and worker/toolchain identity.\n- **Complete results only.** Clean and rejected inspections can be reused. Sanitized entries require an existing artifact with a matching checksum; missing or corrupt artifacts trigger inspection.\n- **Concurrent duplicates share work.** Identical jobs share a lock. If the first request is cancelled, a waiting request may restart inspection.\n- **Bounded retention.** Defaults are 50,000 memory entries, 100,000 persistent entries, 1 GiB of artifacts, and seven-day idle retention. Cleanup runs on admission and every five minutes.\n- **Live purge.** Invalidate memory and disk records and advance the cache generation, preventing older jobs from repopulating it. Already approved requests can finish.\n\n```\npreflight cache status --socket /run/preflight/control.sock\npreflight cache purge --socket /run/preflight/control.sock\n```\n\nMemory and SQLite use entry limits; SQLite does not have a strict physical-byte quota. Rendering scratch space is separate from retained artifacts. Deletion is not forensic erasure from snapshots or backups.\n\nPackaged builds supply the pinned toolchain identity. Development builds disable persistent verdict reuse unless `PREFLIGHT_TOOLCHAIN_ID` is supplied.\n\nThe flake exposes the CLI as a package/app, the complete document-processing runtime, and the devenv shell as `devShells.default`. Package outputs target `x86_64-linux` and `aarch64-linux`.\n\nRun without installing:\n\n```\nnix run github:ghuntley/preflight -- serve\n```\n\nInstall into your profile:\n\n```\nnix profile install github:ghuntley/preflight\n```\n\nUse the development shell:\n\n```\nnix develop --no-pure-eval\ncargo build --locked --bins\n```\n\nThe devenv shell requires `--no-pure-eval` because it inspects the working directory. Package/app builds are pure. `devenv shell` and `devenv test` use the same [`devenv.nix`](https://github.com/ghuntley/preflight/blob/main/devenv.nix).\n\nAdd preflight to your flake inputs and import its module:\n\n```\n{\n  inputs.preflight.url = \"github:ghuntley/preflight\";\n\n  outputs = { nixpkgs, preflight, ... }: {\n    nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {\n      system = \"x86_64-linux\";\n      modules = [\n        preflight.nixosModules.default\n        {\n          services.preflight = {\n            enable = true;\n            listenAddress = \"127.0.0.1\";\n            port = 8081;\n            upstream = \"http://127.0.0.1:8080\";\n            mode = \"redact\";\n            environmentFile = \"/run/secrets/preflight.env\";\n          };\n        }\n      ];\n    };\n  };\n}\n```\n\nThe runtime environment file supplies credentials without putting them in the Nix store. Additional nonsecret TOML options go in `services.preflight.settings`.\n\nThe service uses a dynamic user, private cache/runtime directories, resource limits, and JSON logs to journald. It binds to localhost and leaves the firewall closed by default. The flake also exports `overlays.default`.\n\nValidate the package and QEMU machine test:\n\n```\nnix build .#preflight\nnix build .#checks.x86_64-linux.preflight-vm\n```\n\nThe package builds from `Cargo.lock`. Hegel's bootstrap needs network access, so `nix build` skips `cargo test`; CI runs the full suite through devenv.\n\n- Prompts, matched secrets, authorization values, original filenames, and OCR text are **never logged** by preflight. Generated errors contain static codes and opaque finding IDs; upstream response bodies stream through.\n- Workers run without network access in Bubblewrap PID/mount/network namespaces, with CPU, address-space, file-size, and wall-clock limits. Native stderr is discarded.\n- The worker executable is an internal interface: its stdout carries private extraction data to the parent. Use the proxy CLI for normal operation.\n- Attachment retrieval uses separate credentials from inference. Arbitrary URLs never receive the underclass or file-API credential.\n- Cache storage is private. Never commit credentials, caches, OCR output, or `.hegel` artifacts.\n\n```\ndevenv test\ndevenv shell -- cargo bench --locked --bench inspection\n```\n\n`devenv test` runs formatting, clippy, unit tests, [Hegel](https://hegel.dev) properties, integration tests, differential fixtures, and Code Contracts syntax validation.\n\nTesting covers several layers:\n\n- **Unit tests** check exact rule behavior, supported provider fixtures, parsing, and cache bookkeeping.\n- **Hegel properties** exercise redaction, JSON preservation, policy decisions, coordinate mapping, and cache invariants.\n- **Integration tests** generate JPEGs, PNGs, rotated screenshots, text PDFs, scanned PDFs, and embedded attachments. They run actual native tools and check reconstruction, cache restart/purge, reload, and HTTP forwarding against a mock underclass.\n- **Differential tests** compare GitHub detection semantics with Gitleaks using the same database.\n- **NixOS tests** send generated image/PDF requests through the installed service to a Rust mock server.\n\nFixtures use synthetic credentials. The benchmark exercises benign 100 KiB and 1 MiB text inputs. Tests establish the covered HTTP paths; live provider accounts and universal harness compatibility are outside that verification scope.\n\nAgent conventions and Code Contracts guidance are in [`AGENTS.md`](https://github.com/ghuntley/preflight/blob/main/AGENTS.md). Architecture decisions live in [`docs/adr/`](https://github.com/ghuntley/preflight/blob/main/docs/adr).\n\nThe [weekly workflow](https://github.com/ghuntley/preflight/blob/main/.github/workflows/rules.yml) downloads the latest stable Gitleaks source archive, pins its commit and checksums, refreshes the database, runs validation and benchmarks, and rebuilds preflight. It also runs the NixOS VM test and uploads a Nix closure artifact with provenance.\n\nWhen the snapshot changes, the job opens or updates a PR from `automation/gitleaks-rules` into `main`, then enables squash auto-merge. Normal CI runs on that PR, and GitHub merges it once main's requirements pass. Only the database, upstream license, and provenance manifest are committed. The workflow still rebuilds every week when there is no update. Deployment remains an operator action.\n\nRepository setup:\n\n1. Enable **Allow auto-merge** and**Allow squash merging** under Settings → General.\n2. Protect `main` with required status checks**`test`** and**` build`** from the CI workflow, and enable**Require branches to be up to date before merging** . Required human reviews will still need a human; this automation does not bypass or supply approvals.\n3. Create a fine-grained personal access token restricted to this repository with **Contents: read/write** and**Pull requests: read/write** . Store it as the Actions repository secret**`PREFLIGHT_UPDATE_TOKEN`** . Renew it before expiry.\n4. Once this workflow is on `main` , use Actions →**Weekly rules rebuild** →**Run workflow** to exercise the setup.\n\nThe dedicated token allows PR and post-merge events to trigger CI. The built-in `GITHUB_TOKEN` generally suppresses those runs, so it is not used as a fallback. Its default repository permissions can remain read-only. The updater checks for its token and a protected main before starting. See [ADR 0003](https://github.com/ghuntley/preflight/blob/main/docs/adr/0003-automatic-rules-update-merges.md).\n\nRefresh a specific snapshot locally:\n\n```\ndevenv shell -- cargo run --locked --bin preflight-update-rules -- v8.30.1\n```\n\n[MIT](https://github.com/ghuntley/preflight/blob/main/LICENSE). The vendored Gitleaks database retains its [upstream MIT license](https://github.com/ghuntley/preflight/blob/main/vendor/gitleaks/LICENSE).\n\n```\nsrc/\n  main.rs                  CLI, configuration, approval gate, proxy, streaming\n  scanner.rs               embedded rules, carnet, detection, text redaction\n  document.rs              JSON traversal, nested arguments, source mapping\n  resolver.rs              attachment acquisition and reference materialization\n  attachments.rs           worker orchestration, inspection, reconstruction checks\n  worker.rs                typed worker protocol and coordinate transforms\n  cache.rs                 memory/SQLite verdicts, artifact storage, purge\n  metrics.rs               aggregate counters and timing metrics\n  bin/\n    preflight-worker.rs    isolated native-tool adapters and artifact rebuilding\n    preflight-update-rules.rs  release-pinned Gitleaks database updater\ntests/\n  properties.rs            Hegel properties and regression fixtures\n  integration.rs           document and HTTP integration tests\n  differential.rs          comparison with Gitleaks\n  common/                  generated image/PDF fixtures\nrules/                     explicit default rule profile\nvendor/gitleaks/            upstream database, license, and provenance\nnixos/                     service module and VM test\nbenches/                   text-inspection benchmark\n```\n\n", "url": "https://wpnews.pro/news/a-proxy-that-scans-llm-requests-attach-for-secrets-before-they-reach-the-model", "canonical_source": "https://github.com/ghuntley/preflight", "published_at": "2026-09-20 14:47:47+00:00", "updated_at": "2026-09-20 14:52:45.205975+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "ai-safety", "developer-tools"], "entities": ["preflight", "underclass", "Geoff Huntley", "OpenAI", "Gitleaks"], "alternates": {"html": "https://wpnews.pro/news/a-proxy-that-scans-llm-requests-attach-for-secrets-before-they-reach-the-model", "markdown": "https://wpnews.pro/news/a-proxy-that-scans-llm-requests-attach-for-secrets-before-they-reach-the-model.md", "text": "https://wpnews.pro/news/a-proxy-that-scans-llm-requests-attach-for-secrets-before-they-reach-the-model.txt", "jsonld": "https://wpnews.pro/news/a-proxy-that-scans-llm-requests-attach-for-secrets-before-they-reach-the-model.jsonld"}}