# Keep experiments in sandbox lanes

> Let agents try work on lane branches that run checks against main but never merge into it.

Source: https://getbay.io/docs/lanes (HTML).

A lane is a set of refs, such as `refs/heads/research/*`, for work that must never reach main. A change you open in a lane runs its checks against main like any change. Then bay refuses to merge it, and hands back the verdicts. Agents can try ideas and see the check results, and main stays untouched.

bay also keeps lane work out of other changes. It refuses a change whose history reaches lane commits. The agent hook denies Git commands that copy from a lane ref. bay cannot see bytes copied by hand, such as an editor paste.

## When to use lanes

- **Research spikes**: an agent tries an approach and learns whether the checks pass.
- **Prototypes**: work you plan to rewrite before it reaches main.
- **Risky experiments**: work you want checked against today's main, but never merged.
- **Keep it apart**: bay refuses to merge another change that carries lane commits.

## Declare a lane

Add one `[[lane]]` per lane to `.bay/config.toml` on main. `name` is 1 to 64 characters of `a-z`, `0-9` and `-`. `refs` lists full ref names under `refs/`, each with at most one `*`. A `*` matches any run of characters, slashes too. A pattern never matches main, and never lies under `refs/bay/`. bay also reads a branch pattern under its remote-tracking name, such as `refs/remotes/origin/research/*`.

```toml
[[lane]]
name = "research"
refs = ["refs/heads/research/*"]
window_s = 3600                    # keep an ended lane change's view for one hour
```

## Start a lane change

`bay start --lane <name>` opens a change in that lane. The lane must be one that main's `.bay/config.toml` declares, or bay creates nothing. The result names the lane in `detail.lane`, and `bay status` shows it on the change.

```sh
bay start CARD-17 --lane research --paths src/search/
```

## Run its checks

Run `bay land` as for any change. bay builds the candidate on the current main and runs its checks. A lane change takes no place in the queue, and a red main does not hold it. Then bay refuses it with `LANE_NEVER_LANDS`, and `detail.checks` lists each check's verdict. `detail.check_refusal` names the code the checks gave, such as `CHECK_FAILED`, else `null`. A conflict, or a check that no worker could run, comes back as its own refusal instead.

```sh
bay land --wait --json | jq '.detail.checks'
```

## Keep lane work out of other changes

At `bay land`, bay fetches the lane refs and reads the history of a change outside any lane. It reads the frozen draft, the view's `HEAD`, and any merge or cherry-pick under way there. When that history reaches lane commits that are not on main, bay refuses with `LANE_LEAK`. `detail.leaks` names each lane ref and up to 20 of its commits. To repair, reset the view to the change's base and redo the work, or start it in the lane.

## Hook denials

In the view of a change outside any lane, the agent hook denies Git commands that copy lane work. Each denial is `SHELL_DENIED`, and names the shape it matched. The hook reads a lane ref as a full name, or as Git completes a short one. It also reads commands inside a shell wrapper such as `bash -c`.

| Denied | Example |
|---|---|
| `git merge` of a lane ref | `git merge origin/research/x` |
| `git cherry-pick` of a lane ref | `git cherry-pick main..research/x` |
| `git checkout <lane ref> -- <paths>` | `git checkout research/x -- src/a.rs` |
| `git restore --source <lane ref>` | `git restore --source research/x src/a.rs` |

> A plain `git checkout` of a lane branch runs, but it moves the view's `HEAD`, so `bay land` refuses.
> The hook does not judge `git show <ref>:<path>`, a patch, or an editor paste.

## Lane work merged outside bay

A forge merge can still bring lane commits onto main. `bay status` names that merge under `detail.unattributed`, with `lane` and `lane_leak` on its row. `lane_leaks` counts the merges that name a lane, and the fleet map lists the same. Outside a view, `bay watch` adds `lane` and `lane_leak` to that merge's `unattributed_landing` event. bay names only merge commits this way. A fast-forward or a cherry-pick shows as a plain `outside` row.

## When a lane view goes

A lane change ends when bay refuses it with `LANE_NEVER_LANDS`. A later `bay start` removes its view once the lane's `window_s` has passed. `window_s` defaults to one day, or to the retention window when that is shorter. It must be shorter than `[retention] window_s`, or the config does not load.

## What lanes cannot do

- A lane change never merges. No flag or retry changes that.
- bay cannot detect bytes copied by hand, such as a paste or a file from another clone.
- The hook judges only the four Git shapes above.
- `bay land` catches only copies that carry lane history.

## Refusals

Each refusal is `blocked` and exits `10`. The Results page lists what to do next for each one.

| Code | Meaning |
|---|---|
| `LANE_NEVER_LANDS` | A lane change ran its checks and never merges. `detail.checks` carries the verdicts. On a host from `bay host init` with a key registry, the hook refuses its merge too. |
| `LANE_LEAK` | The change's history reaches lane commits that are not on main. `detail.leaks` names the lane refs and commits. |
| `SHELL_DENIED` | The agent hook denied a Git command that copies from a lane ref. |

## Limits

- **Tips**: one check reads at most 64 lane tips, and `detail.tips_truncated` says when it stopped.
- **Commits**: one leak names at most 20 commits, and counts them all.
- **View**: an ended lane change keeps its view for `window_s`, at most one day by default.
