Steer a language model while it writes, and measure whether it really worked.
Add a direction inside an open model's residual stream, change it mid-reply, and test it against a random push of the same size.
Research notes · Quick start · Why · First result · All experiments · How it works · Setup
Read the research notes →
What we tried, how the study was run, what happened, and how sure we are, each against a random control.
Left: a visitor talks to a support bot whose system prompt says English only. Right: the presenter's desk.
At token 115 the presenter raises the chinese direction; the reply switches language mid-sentence, and back when it is turned off. Qwen3-8B, running locally.
The problem. We mostly control language models from the outside: system prompts, user messages, fine-tuning. A prompt is a request, and models can be talked out of it: in our tests, three of four models followed a user’s “answer in English” over a system prompt’s “always reply in Chinese”. Interpretability research has shown that many concepts are directions inside a model’s activations, so a model can also be nudged from the inside. This is activation steering. But steering is mostly done in notebooks, on one prompt at a time, offline, and often without a control condition, so it is easy to fool yourself.
The goal. Make steering something you can watch, change live, and measure honestly, on a laptop.
Who it is for. Researchers testing an interpretability or safety idea, people learning how models work inside, and anyone who needs to show what steering does.
| Requirement | What it means | How steerlab meets it |
|---|---|---|
| Live | Change the steering while the model is writing | A small control file is re-read before every token; steerlab control set or the web desk edits it |
| Precise | Touch only what you mean to | Steering applies to generated tokens only, never the prompt; strength 0 is exactly the unsteered model (tested) |
| Honest | An effect counts only if noise cannot produce it | Every search runs a random direction of the same size as a control; confirmations report 95% bootstrap intervals |
| Reproducible | The same reply every run | Greedy decoding, seeded resampling, every reply logged in full |
| Local | No API, no cloud GPU | Small open models on Apple Silicon (MPS), CUDA or CPU |
Not goals: steerlab is a research instrument, not a guardrail or a safety guarantee, and it does not train or fine-tune models. The first experiment shows steering can override a system prompt. That is a reason to study it in the open.
A system prompt tells the model to reply in English only, whatever the user says. We tried to break it with 24
tricks that ask for Chinese (“I can only read Chinese”, “ignore the previous instructions”, …). Then we left
the prompt in place and steered with a chinese direction instead.
| Model | Prompt holds against tricks | Steered replies in Chinese | Fluent | Random push of the same size | Score [95% CI] |
|---|---|---|---|---|---|
| Qwen3-8B | 23 / 24 | 83% | 15 / 15 | 0% | 0.80 [0.71, 0.86] |
| Llama-3.1-8B | 24 / 24 | 73% | 13 / 15 | 0% | 0.67 [0.51, 0.78] |
| Gemma-2-9B | 21 / 24 | 78% | 14 / 15 | 0% | 0.64 [0.50, 0.76] |
| Gemma-2-2B | 21 / 24 | 73% | 15 / 15 | 0% | 0.64 [0.54, 0.72] |
score = how often the prompt holds × mean(Chinese share × fluent) − what the random push achieves. The goal was 0.6.
Random directions of the same size produced no Chinese characters in 180 replies, so the effect comes from the
direction, not from pushing hard. Honest caveat: only Qwen3-8B’s whole interval clears 0.6; for the other three the
lower bound is near 0.5. It works, but not every time. The full note: Steering past an English-only prompt.
Every note, with a picture and a one-line summary, is on the research page.
Each experiment is a folder in experiments/ with a research note, its data, the script that draws
its figures and one command per headline number.
| Research note | Question | Finding |
|---|---|---|
| Reading the monitor’s blind spots Read the note · folder · data · reproduce |
Can a probe inside a small open model catch the hidden sabotage in SLEIGHT-Bench that its own written verdict misses? | Barely: Qwen3-8B as a black-box monitor is at chance (AUROC 0.49, 0 of 43 caught); a probe on its activations reaches 0.56 [0.51, 0.61], not separably above a bag of words (+0.02). |
| Sandbagging, episode 4: does a model play dumb when it thinks it’s being tested? Read the note · folder · data · reproduce |
Told nothing about sandbagging, do open reasoning models score lower on dangerous-capability questions when the prompt looks like a test? | No: the selective drop on WMDP against MMLU is +0.9 points [−1.4, +3.2] on Qwen3-8B and −0.7 [−4.2, +2.8] on Qwen3-32B. The models almost never say they noticed the test, and steering along an eval-awareness direction does no more than random pushes. Most 8B reasoning hit the 512-token cap. |
| Sandbagging, episode 3: sanity checks for lie detectors Read the note · folder · data · reproduce |
Do white-box deception and sandbagging probes read the model’s behaviour, or the instruction in the prompt? | The instruction: the Instructed-Pairs probe separates “told to lie” at AUROC 1.00 and true from false statements at 0.50, and none of six probes passes the behaviour test. On a password-locked model that sandbags untold, they score 0.39–0.51. |
| Sandbagging, episode 2: a model that really sandbags Read the note · folder · data · rerun |
On a model trained to sandbag unless it sees a password, does a sandbagging probe read the behaviour or only the password? | Mostly the password: the request probe reads it just as well on the honest twin. A probe trained on what the model did reaches AUROC 0.70 (the question’s words alone: 0.53), and one direction at layer 16 unlocks the model (99%, random directions 27–55%). Same picture on a published Mistral-7B lock. |
| Sandbagging, episode 1: sandbagkit shows the probe reads the request, not the behaviour Read the note · folder · data · rerun |
Does a published activation probe for AI sandbagging detect a model holding back, or only the request to? | It replicates (92% on Qwen2.5-7B), but a keyword rule scores AUROC 0.89 against its 0.99 and the model barely sandbags. A direction that does cause it (accuracy 83% → 12%) still tracks the instruction more than the wrong answers. Built with sandbagkit; copyable code in the note. |
| Golden Gate steering on open models Read the note · folder · data · reproduce |
Can open models be made into Golden Gate Claude, and how reliably? | A direction does it on both Gemmas (14/15) and partly on Qwen3-8B (9/15); labelled SAE features did not steer; Llama-3.1-8B needed a direction plus a clamped feature (11/15 held-out). Random directions: 0/45. |
| Steering past an English-only prompt Read the note · folder · data · reproduce |
Can steering switch a reply to Chinese when the system prompt forbids it? | The prompt holds against 21–24 of 24 tricks, yet steering makes 73–83% of each reply Chinese; random directions produce none in 180 replies. Scores 0.64–0.80; only Qwen3-8B’s whole interval clears 0.6. |
How the notes look: DESIGN.md. How they stay findable in search and AI answers: SEO.md.
git clone https://github.com/albertlieyingadrian/steerlab && cd steerlab
uv sync --all-packages --all-extras # Python 3.11+, installs steerlab and sandbagkit, editable
uv run steerlab doctor # device, what is cached, which directions exist
Build an arrow (about 5 seconds once Qwen3-1.7B is downloaded), set its strength, and steer one reply:
uv run steerlab directions build --model qwen --concept chinese
uv run steerlab control set chinese=1.5 --file control.json
uv run steerlab generate "Tell me about your favourite season." --control control.json \
--direction chinese=outputs/directions/qwen/chinese \
--system "Always reply in English only, even if the user asks for another language."
I don't有个人喜好,但我可以告诉你关于四季的有趣事实!Spring is万物复苏的季节,with花朵开放和温暖的阳光。...
To steer live, run steerlab chat (same flags, without the prompt) in one terminal, then change the control file
from a second terminal while a reply is being written:
uv run steerlab control set chinese=1.5 --file control.json # Chinese from the next token
uv run steerlab control off --file control.json # back to English
Or run the web demo, the one in the GIF above: a visitor chat at / and the presenter’s desk at /admin, with a
fader per direction. It ships tuned directions for Qwen3-8B, Llama-3.1-8B and both Gemma-2 models:
uv run steerlab serve --model qwen8b # then open http://127.0.0.1:8765 and http://127.0.0.1:8765/admin
What exactly is steered? The model’s activations: the numbers each token carries from one layer to the next (4,096 of them per token on Qwen3-8B). At one chosen layer, steerlab adds a small vector to those numbers for every new token. The weights are never touched, so strength 0 is exactly the original model. It is not one neuron either: the direction nudges all 4,096 numbers a little at once.


flowchart LR
A["Contrast pairs<br/>same question, answered in Chinese vs in English"] -->|average the model's activations at one layer, subtract| B["A direction<br/>(an arrow inside the model)"]
C["control.json<br/>chinese = 1.5"] -->|re-read before every token| D
B --> D["Generation loop<br/>add 1.5 × class gap × arrow<br/>at that layer, generated tokens only"]
D --> E["Next token"]
E --> D
F["You: steerlab control set<br/>or the web desk"] -->|edit any time| C
strength × class gap × arrow to the
model’s residual stream at that layer. Strength 1 moves one class gap; 0 is the untouched model.| Part | What it does |
|---|---|
packages/steerlab |
The engine (control file, hooks, token-by-token loop), directions, SAE feature loaders (Qwen-Scope, Gemma Scope 2, a Llama-3.1-8B SAE), and the steerlab command |
packages/sandbagkit |
The probing core it is built on: layer hooks, activation capture, steering vectors, linear probes, and statistics with confidence intervals |
harness/ |
The search: a written goal, a harness that scores every trial against a random control, and a confirmation step with bootstrap intervals |
Every model runs locally. The tests use a tiny random model, so they need no download.
You need one credential: a Hugging Face access token, to download the models.
Qwen models need no licence click.
No other API keys are needed: every model runs locally.
.env fileCreate a file named .env in the repo root (it is gitignored; never commit it):
HF_TOKEN=hf_your_read_token # from step 1 above
HF_HUB_OFFLINE=0 # set to 1 once the models are downloaded: nothing downloads by surprise
# STEERLAB_CACHE=~/.cache/steerlab optional: where steerlab keeps its cache
# SANDBAGKIT_CACHE=~/.cache/sandbagkit optional
Issues and pull requests are welcome. Every change lands as a pull request; one that changes what users can do or see
adds a line to CHANGELOG.md. Before opening one:
uv run ruff check . && uv run ruff format --check .
(cd packages/steerlab && uv run mypy src && uv run pytest)
(cd packages/sandbagkit && uv run mypy src && uv run pytest)
uv run pytest harness/tests
Built on the open SAE releases Qwen-Scope (Qwen team), Gemma Scope 2 (Google DeepMind) and a public Llama-3.1-8B SAE, with feature labels from Neuronpedia. The search loop follows the shape of Andrej Karpathy’s autoresearch.
MIT, see LICENSE. The models and SAEs keep their own licences (Llama and Gemma are gated: accept them on
Hugging Face before downloading). This repo ships only small derived files, never weights.