## Description
RTK CLI-command filtering was set up **by default** across ~16 `wrap`
subcommands (copilot, codex, aider, cursor, cline, continue, goose,
openhands, opencode, grok, omp, openclaude, vibe, …) via `if not
no_rtk:` — so users got rtk hooks / instruction injection without opting
in. `wrap claude` was the lone exception (already gated on
`--context-tool`).
This makes RTK **opt-in (off by default)** everywhere, so Headroom's own
savings are what's measured unless a user explicitly wants rtk.
Closes #
## Type of Change
- [x] Bug fix (behavior change: default flip)
## Changes Made
- **Central gate** `_rtk_opt_in()` — RTK runs only when explicitly
enabled via `--rtk` or `HEADROOM_RTK=1`. Guards the 3 RTK entry points
(`_setup_rtk`, `_ensure_rtk_binary`, `_inject_rtk_instructions`) so they
no-op by default — one small change instead of editing ~30 call sites.
- **`--rtk` opt-in flag** on all 18 tool subcommands via a shared
eager-callback option (`expose_value=False`, sets `HEADROOM_RTK=1`; no
subcommand signature changes).
- `wrap claude`'s legacy `--context-tool` still opts in (mirrored into
the gate).
- **`--no-rtk` kept** as an accepted, now-redundant no-op (back-compat).
- lean-ctx and all non-RTK behavior untouched.
## Testing
```text
pytest tests/test_wrap_rtk_opt_in.py -> 4 passed
ruff check / format -> clean
mypy headroom -> Success: no issues found in 504 source files
```
Verified `--rtk` appears in `wrap {claude,codex,copilot} --help`;
`_rtk_opt_in()` is False by default, True with `HEADROOM_RTK=1`; entry
points no-op + write nothing when off.
## Real Behavior Proof
- Env: local `.venv`, click CliRunner.
- Steps: import wrap; assert gate default-off / env-on; assert
`_setup_rtk`/`_ensure_rtk_binary` return None and
`_inject_rtk_instructions` returns False + writes no file when not opted
in; assert `--rtk` in subcommand help.
- Observed: all pass. Not tested: a live end-to-end wrap launch (proxy
spawn).
## Notes
Part 2 of 3 (RTK opt-in). Separate PRs cover the code-graph MCP engine
and the proxy-option cleanup. No `CHANGELOG.md` edit — release-please
generates it from the PR title (per the changelog guard).
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
## Description
Fixes#1126. On a Windows (cp1252) locale, `headroom wrap` crashes with
`UnicodeDecodeError` the first time it injects guidance into a user
instruction
file that contains non-ASCII prose (e.g. typographic quotes `“happy
places”` or
an em-dash). `_inject_rtk_instructions` and `_inject_memory_agents_md`
both read
the existing file and append/create it with a bare `read_text()` /
`open()` /
`write_text()`, so the default codec (cp1252, not UTF-8) chokes on the
multi-byte characters.
This is the same bug class already fixed for the `learn` pipeline
(#1202) and
earlier for other wrap paths — here it's the instruction-file injectors.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `headroom/cli/wrap.py`: in `_inject_rtk_instructions` and
`_inject_memory_agents_md`, read the existing instruction file as
`encoding="utf-8", errors="replace"` and append/create with
`encoding="utf-8"`. The read only feeds the marker-existence check and
the
append doesn't rewrite existing bytes, so replacement can't corrupt the
file.
- `tests/test_cli/test_wrap_encoding.py`: new regression tests.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_cli/test_wrap_encoding.py tests/test_cli/test_wrap_hintfile_agents.py -q
16 passed
$ ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_encoding.py
All checks passed!
```
The new tests are **red on the old code, green with the fix**: injecting
into a
file with a typographic quote plus a stray `0x9d` byte (undefined in
cp1252 and
invalid UTF-8, so a bare `open()` fails on any locale) — the append and
idempotent paths fail before the fix (4 failed) and pass after (6
passed).
## Real Behavior Proof
- Environment: Windows 11, Python 3.10, against the real
`headroom.cli.wrap`
injectors (no live agent launch; the decode failure is at file read
time).
- Exact command / steps: `write_bytes` an `AGENTS.md` containing
`"Be in “happy places” — really.\n"` plus a stray `0x9d` byte, then call
`_inject_rtk_instructions(path)` / `_inject_memory_agents_md(path)`.
- Observed result: **before** the fix → `UnicodeDecodeError: 'utf-8'
codec
can't decode byte 0x9d` (and on a real cp1252 locale, the same on the
typographic quotes alone); **after** → both return `True`, the marker is
present, the pre-existing prose is preserved, and re-running is
idempotent.
- Not tested: a full end-to-end `headroom wrap copilot` against a live
Copilot
CLI (verified at the injector level, which is where the decode crash
lives).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>