# Tier 2 Sandbox File Leak Prevention — Plan

**Track:** `tier2_leak_prevention_20260620`
**Created:** 2026-06-20
**Status:** SHIPPED (4 atomic commits)

This plan was authored retroactively after the work was completed in-session
(in response to a user request: "tier-2 files leaked into master via commit
00e5a3f2; undo them and add a guard"). The plan is recorded here for
traceability per `conductor/workflow.md` "Plan is the source of truth."

## Phases

### Phase 1: Revert the offender commit (selective)

**Commit:** `fab2e55b fix(tier2): undo sandbox file leaks from 00e5a3f2`

**WHERE:** `git revert -n 00e5a3f2` then surgically unstage files outside the user's scope.

**WHAT:**
- Delete `.opencode/agents/tier2-autonomous.md`
- Delete `.opencode/commands/tier-2-auto-execute.md`
- Revert `mcp_paths.toml` extra_dirs to `["C:/projects/gencpp"]`
- Revert `opencode.json` MCP path to `manual_slop`, default_agent to `tier2-tech-lead`
- Leave at HEAD: 4 throwaway scripts in `scripts/tier2/artifacts/.../*.py`, `project_history.toml` timestamp

**HOW:** `git revert -n` (apply without committing), then `git reset HEAD -- <files>` to unstage the files outside scope, then `git checkout HEAD -- <files>` to restore them to HEAD's content. Resolve the modify/delete conflict on `tier2-autonomous.md` (commit `07f46bfd` modified it after the offender added it) by deletion.

**SAFETY:** User's project-level config files (config.toml, project.toml, etc.) were uncommitted at session start; stashed them as `stash@{0}` (tier2-safety-checkpoint) before the revert to avoid losing them. Commit with explicit message + git note.

### Phase 2: Pre-commit hook + config + tests

**Commit:** `81e1fd7b feat(tier2): add pre-commit hook + denylist config to block sandbox-only files`

**WHERE:**
- NEW `conductor/tier2/githooks/pre-commit`
- NEW `conductor/tier2/githooks/forbidden-files.txt`
- NEW `tests/test_tier2_pre_commit_hook.py`

**WHAT:** A shell script that auto-unstages forbidden files from any tier-2 commit. Configurable via a separate denylist file (one substring pattern per line; `#` comments and blanks ignored).

**HOW:**
1. Write 12 failing tests in `tests/test_tier2_pre_commit_hook.py` (TDD red phase)
2. Write `conductor/tier2/githooks/pre-commit` as a `#!/bin/sh` script
3. Write `conductor/tier2/githooks/forbidden-files.txt` with 4 specific patterns
4. Run tests; verify all 12 pass (green phase)

**SAFETY:**
- Hook always exits 0 (removes the leak rather than blocking the commit; tier-2 cannot run `git restore --staged` per sandbox rules)
- Uses `git rm --cached --force` (NOT `git restore`; required when staged content diverges from HEAD and working tree; discovered during TDD)
- Hook source file is plain POSIX sh; no Python dependency; works under Git Bash on Windows
- 12 tests cover: empty staged set, allowed files, each forbidden file type, multi-file unstaging, mixed staged sets, hook silence, hook warning, config-driven denylist, paths with spaces

### Phase 3: Audit script + tests

**Commit:** `f5d8ea04 feat(audit): add audit_tier2_leaks.py for tier-2 sandbox file leak detection`

**WHERE:**
- NEW `scripts/audit_tier2_leaks.py`
- NEW `tests/test_audit_tier2_leaks.py`

**WHAT:** A Python script that scans the main repo's working tree for files matching the forbidden patterns. Reports any matches as leaks. Default mode is informational (exit 0); `--strict` mode exits 1 on leaks (CI gate).

**HOW:**
1. Write 13 failing tests (TDD red phase)
2. Implement `scripts/audit_tier2_leaks.py` with argparse (--strict, --json flags)
3. Run tests; verify all 13 pass

**SAFETY:**
- Only reports `untracked` and `modified` files (tracked-and-clean files in the main repo are legitimate; patterns are about CONTENT not file existence)
- Skips `tests/`, `conductor/`, `node_modules/`, `.git/`, etc.
- Missing config file: warn to stderr, exit 0 (graceful degradation; hook also no-ops)
- Script uses `git ls-files` and `git diff --name-only` via subprocess; no shell injection risk

### Phase 4: Wire the hook into setup_tier2_clone.ps1

**Commit:** `8f54deda chore(tier2): install pre-commit hook via setup_tier2_clone.ps1`

**WHERE:** `scripts/tier2/setup_tier2_clone.ps1` step 4 (Install git hooks)

**WHAT:** Add `Copy-Item` for the new `pre-commit` hook alongside the existing `pre-push` and `post-checkout` hooks. Existing tier-2 clones need to re-run setup to install the new hook; new clones get it automatically.

**HOW:** Single-line addition to the existing git hooks installation block. The forbidden-files.txt config is already committed to the clone by the canonical-source commit, so the hook can find it via the project root.

**SAFETY:** The copy is idempotent (uses `-Force`). Tested by `tests/test_tier2_setup_bootstrap.py` (3 opt-in tests; all pass with the change).

## Verification

| Test file | Default-on tests | Opt-in tests |
|-----------|------------------|--------------|
| `tests/test_audit_tier2_leaks.py` | 13 | 0 |
| `tests/test_tier2_pre_commit_hook.py` | 12 | 0 |
| `tests/test_tier2_setup_bootstrap.py` | 0 | 3 |
| `tests/test_tier2_sandbox_enforcement.py` | 0 | 1 |
| `tests/test_tier2_slash_command_spec.py` | 17 | 0 |

**Total: 42 default-on + 4 opt-in** (all pass when the right env vars are set).

Manual end-to-end verification: created a fake git repo, staged `opencode.json` with a sandbox-style modification, ran the hook, verified the file was unstaged and the commit proceeded without it.

## Atomic per-task commits

Per `conductor/workflow.md` "ATOMIC PER-TASK COMMITS":

1. `fab2e55b fix(tier2): undo sandbox file leaks from 00e5a3f2` (Phase 1)
2. `81e1fd7b feat(tier2): add pre-commit hook + denylist config to block sandbox-only files` (Phase 2)
3. `f5d8ea04 feat(audit): add audit_tier2_leaks.py for tier-2 sandbox file leak detection` (Phase 3)
4. `8f54deda chore(tier2): install pre-commit hook via setup_tier2_clone.ps1` (Phase 4)

Each commit has a `git notes add -m "..." <sha>` summary explaining the why (per the workflow).
