Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when inspecting or intervening on transformer internals with TransformerLens (loading, activation caching, hook-point names, forward hooks, tokenization, config and weight access): emit the current API grammar (HookedTransformer.from_pretrained, run_with_cache, the blocks.{L}.hook_* / attn./mlp. hook names, run_with_hooks) — the exact tokens cheaper models emit in wrong, outdated, or HuggingFace-flavored forms.
.claude/skills/transformer-lens-interpretability/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 28 |
| gemini-3.1-pro-preview | 100% | 1 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +23% | +121% | 0% | 22 | 53d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
Enforces the current TransformerLens public API when the task is to load a model, cache activations, read a specific internal activation, or intervene on the forward pass for mechanistic-interpretability work. The library exposes every activation through a named HookPoint, and the names follow one deterministic grammar. Apply whenever the task inspects or edits transformer internals with TransformerLens.
from transformer_lens import HookedTransformer, thenmodel = HookedTransformer.from_pretrained("gpt2-small"). Never the bare constructor HookedTransformer("gpt2"), never HookedTransformer.load(...), and never HuggingFace's AutoModelForCausalLM.from_pretrained — you lose the hooks.
run_with_cache, which returns a tuple. One call captures every intermediateactivation: logits, cache = model.run_with_cache(tokens). It returns exactly two values — the logits and the ActivationCache. Do NOT use HuggingFace output_hidden_states=True / output_attentions=True, and do NOT hand-register PyTorch hooks to collect activations.
blocks.{L}.[<submodule>.]hook_<name> where {L} is the layer index. The hook_ prefix is mandatory. Whether there is a submodule depends on WHERE the activation lives:
blocks.{L}.hook_resid_pre, blocks.{L}.hook_resid_mid, blocks.{L}.hook_resid_post, blocks.{L}.hook_attn_out, blocks.{L}.hook_mlp_out.
attn.: blocks.{L}.attn.hook_q, hook_k, hook_v,hook_z, hook_pattern (post-softmax weights), hook_attn_scores (pre-softmax), hook_result.
mlp.: blocks.{L}.mlp.hook_pre, blocks.{L}.mlp.hook_post.ln1/ln2/ln_final: blocks.{L}.ln1.hook_normalized,blocks.{L}.ln1.hook_scale, and the final one ln_final.hook_normalized.
hook_embed, hook_pos_embed.Two easy-to-miss rules: mlp_out and attn_out sit on the BLOCK (blocks.{L}.hook_mlp_out), NOT under mlp./attn.; and ln_final.hook_normalized has no blocks.{i}. prefix.
cache["blocks.5.hook_resid_post"]equals cache["resid_post", 5]. The shorthand is (short_name, layer). Useful shorthands: cache["resid_post", L], cache["pattern", L], cache["z", L], cache["q"/"k"/"v", L], cache["mlp_out", L], cache["attn_scores", L].
names_filter. To cache a subset, pass a predicate over thename: model.run_with_cache(tokens, names_filter=lambda name: "resid_post" in name). To offload the captured tensors, pass device="cpu".
run_with_hooks. To edit activations for one run:model.run_with_hooks(tokens, fwd_hooks=[(name, hook_fn)]). fwd_hooks is a LIST of (hook_name, function) tuples. Never PyTorch's module.register_forward_hook.
(activation, hook) and returns the activation.python def hook_fn(activation, hook): activation[:] = 0.0 # edit in place (or build a new tensor) return activation # must return it The two parameters are the activation TENSOR and the HookPoint object (conventionally named hook; hook.name is its name). This is NOT the PyTorch (module, input, output) signature.
add_hook (or a run that did not clean up) persist.Call model.reset_hooks() for a clean state before the next run.
model.to_tokens(text) → token tensor;model.to_single_token(" Paris") → one id; model.to_str_tokens(text) → list of piece strings; model.to_string(tokens) → text. Control the leading BOS with prepend_bos=False. Prefer these over a raw HuggingFace tokenizer.
model.cfg; weights are named properties. Shapes and counts live on theHookedTransformerConfig: model.cfg.n_layers, model.cfg.n_heads, model.cfg.d_model, model.cfg.d_head — NOT model.config.num_hidden_layers / hidden_size. Weights are exposed directly: model.W_U (unembed), model.W_E (token embed), model.W_pos (positional), model.W_Q, model.W_K, model.W_V, model.W_O, model.W_in, model.W_out. The per-head stacks are indexed [layer, head], e.g. model.W_O[9, 4].
patching helpers for causal tracing. from transformer_lens import patching,then patching.get_act_patch_resid_pre(...) (and the get_act_patch_attn_out / get_act_patch_mlp_out / get_act_patch_attn_head_* family) rather than hand-rolling the patch loop.
BEFORE = the base model's wrong default → AFTER = the conforming TransformerLens grammar.
Load + cache.
python# BEFORE from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("gpt2") out = model(tokens, output_hidden_states=True) # not the TL way # AFTER from transformer_lens import HookedTransformer model = HookedTransformer.from_pretrained("gpt2-small") logits, cache = model.run_with_cache(tokens) # (logits, cache) tuple
Read an activation.
python# BEFORE resid = cache["blocks.5.residual"] # wrong name patt = cache["blocks.3.attention.weights"] # wrong name # AFTER resid = cache["resid_post", 5] # == cache["blocks.5.hook_resid_post"] patt = cache["pattern", 3] # == cache["blocks.3.attn.hook_pattern"] mlp = cache["mlp_out", 7] # == cache["blocks.7.hook_mlp_out"] (block level!)
Selective / offloaded cache.
python# AFTER logits, cache = model.run_with_cache( tokens, names_filter=lambda name: "resid_post" in name, device="cpu", )
Intervene (ablate a head output).
python# BEFORE (torch native) def hook(module, input, output): # wrong signature return output * 0 handle = model.blocks[6].register_forward_hook(hook) # AFTER def ablate(activation, hook): activation[:] = 0.0 return activation logits = model.run_with_hooks( tokens, fwd_hooks=[("blocks.6.hook_attn_out", ablate)] ) model.reset_hooks()
Tokenize + config + weights.
python# AFTER tokens = model.to_tokens("The Eiffel Tower is in", prepend_bos=True) paris = model.to_single_token(" Paris") for layer in range(model.cfg.n_layers): for head in range(model.cfg.n_heads): contribution = cache["z", layer][0, -1, head] @ model.W_O[layer, head] @ model.W_U
resid_pre vs resid_mid vs resid_post. resid_pre is before attention, resid_mid isafter attention / before MLP, resid_post is after MLP (the block's output). Pick by which one the analysis needs; all three are block-level (blocks.{L}.hook_resid_post).
pattern vs attn_scores. hook_pattern is post-softmax (a probability distribution);hook_attn_scores is the pre-softmax logits. They are different hooks under attn..
z vs attn_out. hook_z (under attn.) is per-head, shape [batch, pos, head, d_head],before the output projection; hook_attn_out (block level) is the summed, projected attention output, shape [batch, pos, d_model].
cache["attn", L] resolves to hook_pattern; cache["key"/"query"/"value", L]resolve to hook_k/hook_q/hook_v. Prefer the canonical short names.
return activation; ahook that returns None leaves the activation unchanged.
AutoModelForCausalLM.from_pretrained. ALWAYS HookedTransformer.from_pretrained.output_hidden_states=True. ALWAYS run_with_cache (a tuple).hook_ prefix or guess blocks.L.residual. ALWAYS blocks.{L}.hook_resid_post.mlp_out/attn_out under mlp./attn.. They sit at block level.ln_final with blocks.{i}.. It is top-level: ln_final.hook_normalized.register_forward_hook or the (module, input, output) signature. ALWAYSrun_with_hooks(fwd_hooks=[(name, fn)]) with def fn(activation, hook): … return activation.
model.config.num_hidden_layers / hidden_size. ALWAYS model.cfg.n_layers /model.cfg.d_model.
model.lm_head.weight. ALWAYS model.W_U (and W_E/W_pos/W_O/…).AutoModelForCausalLM and losing the hooks.run_with_cache as if it returned only the cache (it returns (logits, cache)).blocks.5.residual, blocks.3.attention.weights,layers[5].attn_pattern, blocks.7.mlp.hook_out for the MLP output.
(module, input, output) hook signature instead of (activation, hook), or forgettingto return the activation.
model.reset_hooks() so edits leak into later runs.model.config.num_hidden_layers / hidden_size instead of model.cfg.n_layers /d_model.
model.lm_head.weight instead of model.W_U.HookedTransformer.from_pretrained("gpt2-small") (not AutoModelForCausalLM).logits, cache = model.run_with_cache(tokens) (a (logits, cache) tuple).blocks.{L}.hook_resid_post / blocks.{L}.attn.hook_pattern / …attn.hook_z /blocks.{L}.hook_mlp_out / blocks.{L}.mlp.hook_pre / ln_final.hook_normalized.
cache["resid_post", L], cache["pattern", L], cache["z", L].names_filter= to subset, device="cpu" to offload.run_with_hooks(fwd_hooks=[(name, fn)]); def fn(activation, hook): … return activation.model.reset_hooks() between runs.to_tokens / to_single_token / to_str_tokens, prepend_bos=False when needed.model.cfg.n_layers / n_heads / d_model; model.W_U / W_O / W_E / W_pos.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/9/2026 | +19% |
Other measured skills in the registry, with their headline benchmark lift.