# fix\(todo-gate\): refuse redirects, bound reads and git probes · PR \#368 · lucenx/codexbar-plasma

[View on GitCafe](https://git.cafe/lucenx/codexbar-plasma/pull/368)

Repository: [lucenx/codexbar-plasma](https://git.cafe/lucenx/codexbar-plasma)

Visibility: public

State: merged

Source repository: lucenx/codexbar-plasma

Source branch: fix/todo-gate-harden

Target branch: main

Head commit: 90c873030bd4694e86a8252037e55b6333bbff25

[Discussion and reviews](https://git.cafe/lucenx/codexbar-plasma/pull/368)

[Commits](https://git.cafe/lucenx/codexbar-plasma/pull/368/commits?format=markdown)

[Files changed](https://git.cafe/lucenx/codexbar-plasma/pull/368/diffs?format=markdown)

## Description

---

## Summary

- The TODO gate POSTed to the Decisions endpoint with plain `urlopen`: a 302 was followed with the `Authorization` header, leaking `OPENROUTER_API_KEY` to the redirect target, and the redirected body was accepted as the verdict. Reproduced against a local server (302 to `/evil` received the bearer key).
- `ask()` now refuses redirects with a `NoRedirect` opener (same pattern as the AI Insights helper) and caps response reads at 64 KiB; the git probes carry a 120 s timeout, and a git timeout skips the gate (exit 0) instead of crashing it.
- Observable result: a redirecting or oversized gate response now reports "TODO gate unavailable" (exit 2); a hung git now skips instead of hanging or tracebacking.

## Related issue

None

## Documentation impact

- No maintained guide documents the TODO gate helper (no hits in `docs/`); the contributor workflow is unchanged (same `make todo-gate` command, same success/skip exit codes).
- TODO.md is unaffected: no parity entry covers helper hardening.
- Changelog: updated Unreleased.

## Verification

- New tests in `tests/test_todo_gate.py` (`AskTests` local-server redirect/oversize/happy-path, `ReadChangeTests` timeout kwargs + timeout skip): observed red before the fix (3 failures + 1 error), green after.
- `make check`: full suite green locally on `90c8730`. Skipped locally (tools absent, CI covers them): `actionlint`, `pyflakes`. QML hardening noted partial import/type checks locally ("CI requires full checks").
- CI: green on `90c8730` — `check`, `smoke`, `smoke-runtime`, CodeQL, `pr-title` all pass ([run](https://github.com/Lucenx9/codexbar-plasma/actions/runs/36062215263)).

## Visual changes

None

## Review notes

- The refused redirect surfaces as `HTTPError`, which `main()` already catches via `URLError` and reports as unavailable (exit 2, fail-closed).
- `TimeoutExpired` is a `SubprocessError`, not a `CalledProcessError`, hence the widened `except` in `main()`.

