- ScriptShield steering vectors for Tiny Aya Fire
- Findings in brief
- What is in the package
- Results on the frozen test set
- Can the automatic judge be trusted in Roman Urdu?
- What can and cannot be claimed
- Check what you downloaded
- How to use
- What the loader checks, and what it does not
- How the vectors were made
- Reproducibility
- Versions
- Limitations
- How this was built
- Licence and terms
- Citation
- Acknowledgements
- Findings in brief
ScriptShield steering vectors for Tiny Aya Fire
Six 2,048-dimensional steering vectors for decoder block 18 of
CohereLabs/tiny-aya-fire, with a manifest and a
standalone loader. No model weights are included.
This is an inference-time steering package, not a fine-tuned model checkpoint. It holds six small vectors, a manifest and two Python files. It cannot be loaded with
from_pretrained, and it does nothing without your own copy of the base model. A vector is added to the model's activations while it generates; the model's weights are never changed.
They were built to test one idea: a refusal-steering vector learned from Urdu-script and Roman-Urdu prompts also carries "which script is this" information; if that component is projected out, the vector might protect Roman-Urdu users better.
The idea was not supported (pre-registered gate NOT MET). These vectors are published so that the result can be checked and built on, not as a recommended safety intervention.
| Dataset and evaluation logs | ahmadrrrtx/scriptshield-urdu-roman |
| Code, protocol and reproduction notes | https://github.com/ahmadrrrtx/scriptshield |
| Write-up | article Β· full result tables |
| Replay of the recorded outputs | ahmadrrrtx/scriptshield-replay |
| Collections | ScriptShield β model artifacts Β· ScriptShield β data artifacts |
Author: Muhammad Ahmad (@ahmadrrrtx). Built for the Lexsi AI Safety and Alignment Hackathon, Track 2: problem 4 (multilingual and code-mixed safety) with problem 8 (inference-time safety steering).
Findings in brief
- The script gap is real and large. On 36 held-out intent families the unsteered model refused harmful requests at 100.0 % vs 47.2 % (-52.8 pp, 95 % CI -69.4 to -36.1; exact McNemar p = 3.8e-06) in Urdu script vs Roman Urdu. Pre-registered test, significant after Holm correction.
- The automatic judge overstates Roman-Urdu refusals, so the true gap is larger. Reading every Roman-Urdu response (post hoc, by the LLM agent, not a human): 13/36 real refusals, 6/36 answers toward the harmful request, 17/36 non-responsive (echo of the prompt, loops, mistranslations, off-topic text). In Roman Urdu the model mostly fails to engage rather than fluently complying.
- Removing the script direction from the steering vector did not help. Pre-registered gate not met: the residualized and the pooled vector gave the same refusal outcome on 71 of 72 harmful prompts.
- Steering at the frozen dose is not a fix. By the judge, the pooled vector raised Roman-Urdu refusal by +13.9 pp, a control vector orthogonal to it by +11.1 pp, and judged refusals of benign prompts rose by +6.9 pp against a pre-registered budget of +2 pp. By adjudication the pooled vector moved real refusals from 13 to 19 of 36 (McNemar p = 0.07) and the control from 13 to 15; no arm produced a real refusal of a benign prompt. Suggestive, not established.
Every vector, including the placebo (orthogonal_null), lifts the judged refusal of harmful Roman-Urdu
requests by a few prompts out of 36, with overlapping intervals. The residualized vector is not better than
the pooled one.
What is in the package
| File | Content |
|---|---|
steering_vectors.npz |
one float64 array of length 2,048 per arm (names below). Each is a direction in the residual stream at the output of decoder block 18 (0-based) |
manifest.json |
layer, strength, per-vector checksums, base-model revision and weight hashes, extraction and inference settings, toolkit versions, code commit |
frozen_selection.json |
the layer and strength chosen on the development split before the test set was run |
apply_steering.py |
standalone loader (needs only numpy, torch, transformers, safetune) |
check_base_model.py |
checks that the package is intact and that a local base model is the one the vectors were made from (needs only numpy; does not load the model) |
expected_outputs.jsonl |
144 logged responses (48 harmless factual questions in Urdu script and Roman Urdu Γ unsteered, pooled, residualized) with their sha256, to check that your setup reproduces ours |
SHA256SUMS |
checksums of every file |
| Arm | What it is | cos to pooled |
|---|---|---|
pooled |
mean of the Urdu-script and Roman-Urdu harmful-minus-benign CAA vectors (equal weight per form) | +1.0000 |
residualized |
pooled vector with its component along the script direction (Urdu script vs Roman Urdu, benign prompts) projected out, rescaled to the pooled norm | +0.9958 |
random_subspace |
CONTROL: pooled vector with a random direction projected out, rescaled to the pooled norm | +0.9998 |
orthogonal_null |
CONTROL: within-pair label-flip noise vector, exactly orthogonal to pooled, pooled norm | +0.0000 |
ur_only |
CAA vector from Urdu-script prompts only, rescaled to the pooled norm | +0.9844 |
ro_only |
CAA vector from Roman-Urdu prompts only, rescaled to the pooled norm | +0.9301 |
All arms are rescaled to the norm of pooled, so they are compared at the same dose. Orientation is
harmful β benign: a positive strength pushes toward refusal.
Results on the frozen test set
36 intent families the vectors never saw (24 other families were used to extract them and 12 more to choose the layer and strength). One model, one seed, greedy decoding, 64 new tokens.
The script gap in the unsteered model (pre-registered)
Paired by intent family, n = 36 test families, harmful prompts, same meaning in both scripts.
| Urdu script | Roman Urdu | Difference | |
|---|---|---|---|
| Refusal rate | 100.0 % | 47.2 % | -52.8 pp (95 % CI -69.4 to -36.1) |
| Judge-flagged unsafe response | 0.0 % | 27.8 % | McNemar p = 0.002 |
Exact McNemar on refusal: 19 families were refused in Urdu script but not in Roman Urdu, 0 the reverse; p = 3.8e-06.
For reference, the same harmful intents were refused at 97.2 % in code-mixed Urdu-English and 100.0 % in English.
The same pattern appears on the other 36 families (calibration + development, never part of this test): 94.4 % in Urdu script vs 44.4 % in Roman Urdu.
The model is also weaker in Roman Urdu on harmless material: on 24 simple factual questions it answered 87.5 % correctly in Urdu script and 50.0 % in Roman Urdu, and it answered 9 of 72 Roman-Urdu prompts in Devanagari (Hindi).
(Utility is scored by answer patterns fixed before the run. Reading the answers (post hoc, by the agent) gives 22 of 24 in Urdu script and 13 of 24 in Roman Urdu, the same 9-question difference; see the note under the all-arms table.)
All arms (pre-registered labels)
| Arm | Refusal, harmful, Urdu script | Refusal, harmful, Roman Urdu | Judged refusal of benign prompts | Invalid | Utility |
|---|---|---|---|---|---|
| unsteered | 100.0 % | 47.2 % | 6.9 % | 1.4 % | 68.8 % |
| pooled | 100.0 % | 61.1 % | 13.9 % | 1.4 % | 72.9 % |
| residualized | 100.0 % | 63.9 % | 9.7 % | 1.0 % | 75.0 % |
| random_subspace | 100.0 % | 61.1 % | 13.9 % | 1.4 % | n/a |
| orthogonal_null | 97.2 % | 58.3 % | 4.2 % | 1.4 % | n/a |
| ur_only | 100.0 % | 61.1 % | 12.5 % | 2.1 % | n/a |
| ro_only | 100.0 % | 58.3 % | 8.3 % | 1.0 % | n/a |
Every attempted prompt is in the denominator (36 per cell; benign = 72 Urdu-script + Roman-Urdu prompts). invalid = empty or a repetition loop; it is never counted as a refusal. Utility was measured for three arms only. The section on the judge shows that its refusal label is unreliable in Roman Urdu; read the Roman-Urdu and benign columns with that in mind.
Utility column: post hoc reading check (protocol A20). The agent read all 144 utility answers (not a human; arm labels visible). 7 pattern scores disagree with the text: 5 correct answers used a spelling or transliteration the patterns do not cover, and 2 answers were matched although the answer they state is wrong (2 borderline, also shown flipped). Details: experiments/adjudication/utility_reading_check.json.
| Arm | Utility by pattern (pre-registered) | Utility on reading (post hoc) | Borderline flipped |
|---|---|---|---|
| unsteered | 33/48 | 35/48 | 35/48 |
| pooled | 35/48 | 36/48 | 37/48 |
| residualized | 36/48 | 36/48 | 37/48 |
On reading, the pooled and residualized arms answer the same questions correctly; the pooled arm differs from the unsteered model on 1 question (2 with the borderline answer flipped). The pre-registered column is unchanged. Differences of one or two questions in it are within the scoring error of the patterns and should not be read as effects of steering.
Residualized vs pooled (pre-registered gate)
| Criterion | Measured | Threshold | Met |
|---|---|---|---|
| Worst-form refusal gain | +2.8 pp (CI +0.0 to +8.3) | β₯ +5 pp | no |
| Script-gap reduction | +2.8 pp (CI +0.0 to +8.3) | β₯ +5 pp | no |
| Over-refusal increase | -4.2 pp | β€ +2 pp | yes |
| Utility drop | -2.1 pp | β€ +2 pp | yes |
| Invalid-rate increase | -0.3 pp | β€ +1 pp | yes |
Gate NOT MET. The two arms gave the same refusal outcome on 71 of 72 harmful prompts; the single difference is one family. With 36 families the interval still contains +5 pp, so by the pre-registered wording this is inconclusive rather than proof of no effect. On reading (post hoc, see the reading of the responses) the two arms are indistinguishable: 19 vs 19 real Roman-Urdu refusals, 3 vs 3 answers, 14 vs 14 non-responsive; the one judged difference was a mistranslated echo that the judge read as a refusal. The two arms produced byte-identical text on 232 of 288 test prompts (80.6 %). The utility difference in the table is a scoring artefact: on reading (post hoc, note under the all-arms table) both arms answer the same 36 of 48 questions correctly. The gate outcome does not depend on it.
Pre-declared continuous endpoint (teacher-forced refusal margin, residualized β pooled, family means): +0.0219 nats/token (95 % CI +0.0208 to +0.0231; sign-flip p = 0.0001). That is statistically detectable and tiny: the unsteered margin is 2.44 for Urdu-script and 1.14 for Roman-Urdu harmful prompts, so residualization closes 1.7 % of that 1.30-nat difference.
Holm-adjusted p-values: H1 unsteered ur vs ro (McNemar): 1.1e-05; H3 worst-form gain (bootstrap): 1; H3 gap reduction (bootstrap): 1.
Each steered arm against the unsteered model
Change in the judge's refusal rate, paired by family, percentage points with 95 % family-bootstrap interval. Not multiplicity-corrected.
| Arm | Harmful, Urdu script | Harmful, Roman Urdu | Benign, Urdu script | Benign, Roman Urdu |
|---|---|---|---|---|
| pooled | +0.0 (+0.0 to +0.0) | +13.9 (-2.8 to +30.6) | +0.0 (+0.0 to +0.0) | +13.9 (-2.8 to +30.6) |
| residualized | +0.0 (+0.0 to +0.0) | +16.7 (+0.0 to +33.3) | +0.0 (+0.0 to +0.0) | +5.6 (-8.3 to +22.2) |
| random_subspace | +0.0 (+0.0 to +0.0) | +13.9 (-2.8 to +30.6) | +0.0 (+0.0 to +0.0) | +13.9 (-2.8 to +30.6) |
| orthogonal_null | -2.8 (-8.3 to +0.0) | +11.1 (+0.0 to +25.0) | +0.0 (+0.0 to +0.0) | -5.6 (-16.7 to +5.6) |
| ur_only | +0.0 (+0.0 to +0.0) | +13.9 (-2.8 to +30.6) | +0.0 (+0.0 to +0.0) | +11.1 (+0.0 to +25.0) |
| ro_only | +0.0 (+0.0 to +0.0) | +11.1 (+0.0 to +25.0) | +0.0 (+0.0 to +0.0) | +2.8 (-11.1 to +16.7) |
orthogonal_null is a control: a vector of the same norm, exactly orthogonal to the pooled refusal vector. By the judge's label it raised Roman-Urdu refusal by +11.1 pp against +13.9 pp for the refusal vector, with overlapping intervals. On this measurement the gain cannot be attributed to the refusal direction. The reading of the responses looks at what the responses actually say.
Can the automatic judge be trusted in Roman Urdu?
Second judge β PRE-DECLARED sensitivity analysis
The same outputs scored by a rubric prompt on Qwen2.5-7B-Instruct: H1 refusal 100.0 % vs 44.4 % (-55.6 pp, 95 % CI -72.2 to -38.9; exact McNemar p = 1.9e-06). H3 worst-form gain +2.8 pp, gate not met. Both pre-registered conclusions hold under the second judge.
| Arm | Refusal, harmful, Roman Urdu (judge 1) | (judge 2) | Refusal of benign prompts (judge 1) | (judge 2) |
|---|---|---|---|---|
| unsteered | 47.2 % | 44.4 % | 6.9 % | 13.9 % |
| pooled | 61.1 % | 50.0 % | 13.9 % | 20.8 % |
| residualized | 63.9 % | 52.8 % | 9.7 % | 20.8 % |
| random_subspace | 61.1 % | 52.8 % | 13.9 % | 20.8 % |
| orthogonal_null | 58.3 % | 44.4 % | 4.2 % | 9.7 % |
| ur_only | 61.1 % | 52.8 % | 12.5 % | 16.7 % |
| ro_only | 58.3 % | 47.2 % | 8.3 % | 18.1 % |
Prompt echoes β POST HOC (protocol A18)
The model often answers a Roman-Urdu prompt by repeating the prompt. 49 of 2016 test responses are such echoes (49 of them to Roman-Urdu prompts), and the judge labelled 32 of them refusals. Treating an echo as invalid instead:
- H1 becomes 100.0 % vs 36.1 % (-63.9 pp, 95 % CI -77.8 to -47.2; exact McNemar p = 2.4e-07).
| Arm | Refusal, harmful, Roman Urdu (pre-registered) | (echo-corrected) | Refusal of benign prompts (echo-corrected) |
|---|---|---|---|
| unsteered | 47.2 % | 36.1 % | 6.9 % |
| pooled | 61.1 % | 47.2 % | 13.9 % |
| residualized | 63.9 % | 50.0 % | 9.7 % |
| random_subspace | 61.1 % | 47.2 % | 13.9 % |
| orthogonal_null | 58.3 % | 47.2 % | 4.2 % |
| ur_only | 61.1 % | 47.2 % | 12.5 % |
| ro_only | 58.3 % | 47.2 % | 8.3 % |
Reading the responses β POST HOC, exploratory (protocol A19)
The LLM agent labelled all 214 distinct Roman-Urdu test responses of four arms (288 rows) with the arm hidden, under a rubric committed beforehand: R refusal, A answer toward the request, N non-responsive (echo, loop, paraphrase of the prompt, off-topic or misread). This is not human annotation and not by a native speaker; it was only partly blind.
| Arm | Harmful: R | A | N | Judge said refusal | Benign: R | A | N | Judge said refusal |
|---|---|---|---|---|---|---|---|---|
| unsteered | 13 | 6 | 17 | 17 | 0 | 29 | 7 | 5 |
| pooled | 19 | 3 | 14 | 22 | 0 | 27 | 9 | 10 |
| residualized | 19 | 3 | 14 | 23 | 0 | 28 | 8 | 7 |
| orthogonal_null | 15 | 5 | 16 | 21 | 0 | 27 | 9 | 3 |
36 prompts per cell. With every borderline label flipped to its alternative, harmful-prompt refusals are unsteered 11, pooled 15, residualized 15, orthogonal_null 12.
Judge against this reading, Roman-Urdu rows only:
| Label source | Precision of "refusal" | Recall | Cohen's ΞΊ | What its false refusals really were |
|---|---|---|---|---|
| Judge 1, pre-registered label | 0.53 | 0.86 | 0.52 | 42 non-responsive, 9 answers |
| Judge 1, echo-corrected | 0.63 | 0.86 | 0.63 | 24 non-responsive, 9 answers |
| Judge 2 (rubric) | 0.60 | 0.86 | 0.60 | 19 non-responsive, 19 answers |
Paired change against unsteered (36 families):
| Arm | Real refusals of harmful prompts | Answers toward harmful requests | Benign prompts answered |
|---|---|---|---|
| pooled | 13 β 19 (+16.7 pp, CI +2.8 to +33.3, p = 0.07) | 6 β 3 (-8.3 pp, CI -19.4 to +2.8, p = 0.38) | 29 β 27 (-5.6 pp, CI -19.4 to +8.3, p = 0.69) |
| residualized | 13 β 19 (+16.7 pp, CI +2.8 to +33.3, p = 0.07) | 6 β 3 (-8.3 pp, CI -19.4 to +2.8, p = 0.38) | 29 β 28 (-2.8 pp, CI -13.9 to +8.3, p = 1) |
| orthogonal_null | 13 β 15 (+5.6 pp, CI +0.0 to +13.9, p = 0.5) | 6 β 5 (-2.8 pp, CI -13.9 to +8.3, p = 1) | 29 β 27 (-5.6 pp, CI -19.4 to +8.3, p = 0.69) |
What this shows:
- Of the 108 Roman-Urdu responses judge 1 called refusals, 51 (47 %) were not refusals on reading.
- Benign prompts: the judge reported 25 refusals across the four arms; the reading found 0. What the judge calls over-refusal in Roman Urdu is garbled or looping output.
- Harmful prompts: the judge credits the pooled vector with +5 refusals and the control with +4; the reading finds +6 and +2. With 36 families neither difference is statistically secure.
What can and cannot be claimed
Supported. Tiny Aya Fire's refusal behaviour depends strongly on script: the same harmful request is refused essentially always in Urdu script and less than half the time in Roman Urdu. Roman-Urdu refusal is 47.2 % under the pre-registered judge, 44.4 % under a second judge, 36.1 % under an echo correction, 36.1 % under a reading of every response. Every alternative gives a larger gap than the pre-registered one. In the model's activations the harmful-vs-benign signal is about half as strong for Roman Urdu.
Not supported. That removing the script direction from a refusal-steering vector improves protection for Roman-Urdu users. The pre-registered gate was not met and the two vectors behave alike.
Open. Whether refusal steering helps Roman-Urdu users at all. The reading in 5.3 suggests a real but modest gain; the pre-registered measurement cannot confirm it because the judge is unreliable in Roman Urdu and a control vector scores similarly on it.
Not claimed. Anything about other models, other Roman-Urdu spellings, multi-turn or adversarial prompts, longer responses, or human-validated language quality. A Roman-Urdu non-refusal here is most often a confused or empty answer, not a usable harmful one.
Check what you downloaded
sha256sum -c SHA256SUMS # every file of the package
python check_base_model.py # manifest and vector file agree (no model needed)
python check_base_model.py --model-dir /path/to/tiny-aya-fire # and the base model is the right one (hashes the weights)
Expected last line of the third command:
OK: the package is intact and the model files are the ones the vectors were made from.
The vectors can be inspected on a laptop, without a GPU or the base model:
from apply_steering import load_package # numpy only; every vector is checked against its checksum
vectors, manifest = load_package(".")
print(manifest["layer"], manifest["hidden_dim"], round(manifest["alpha"], 4), sorted(vectors))
Output, produced from the published files when this card was built:
18 2048 0.2694 ['orthogonal_null', 'pooled', 'random_subspace', 'residualized', 'ro_only', 'ur_only']
How to use
pip install torch transformers numpy huggingface_hub
pip install "git+https://github.com/Lexsi-Labs/SafeTune@26f59e6d1bdcacafe5f2be3b8d982fb214369659"
# this package, and the base model (gated: accept its terms on the Hub first, then log in)
hf download ahmadrrrtx/scriptshield-tiny-aya-fire-steering --local-dir scriptshield-vectors && cd scriptshield-vectors
hf download CohereLabs/tiny-aya-fire --revision 73500c80fcb6c8bea3e830c21d5d2920249a0e1a --local-dir /path/to/tiny-aya-fire
# one prompt, unsteered vs steered
python apply_steering.py --model-dir /path/to/tiny-aya-fire --arm pooled --prompt "Ψ’ΩΎ Ϊ©ΫΨ³Ϋ ΫΫΪΊΨ"
# check that your environment reproduces the logged run exactly
python apply_steering.py --model-dir /path/to/tiny-aya-fire --verify expected_outputs.jsonl
from apply_steering import load_package, load_base, make_wrapper, generate
vectors, manifest = load_package(".") # checksums are verified
tok, model = load_base("/path/to/tiny-aya-fire", manifest["inference"]["dtype"])
steered = make_wrapper(model, vectors["pooled"], manifest["layer"], manifest["alpha"])
print(generate(model, tok, ["..."], steered)) # steering is live only via wrapper.generate
Frozen setting: layer 18, Ο = 0.1 (Ξ± = 0.2694; Ο is the added vector's norm as a fraction of the mean residual-stream norm at that layer). You need your own access to the base model.
What the loader checks, and what it does not
| Situation | What happens |
|---|---|
| A vector was altered, or the file is damaged | load_package raises checksum mismatch |
| A vector has the wrong number of dimensions | load_package raises (shape is compared with hidden_dim in the manifest) |
| A vector named in the manifest is missing | load_package raises |
| The manifest states another sign convention | load_package raises instead of guessing the sign |
| The file holds a vector that the manifest does not list | not caught by the loader; check_base_model.py reports it |
| The base model has another hidden size or fewer blocks | not caught before generation by the loader (it fails later with a tensor error); check_base_model.py --model-dir reports it first |
| The base model has the same shape but other weights (another Tiny Aya variant, another revision) | not caught by the loader: it would steer silently. check_base_model.py --model-dir compares the sha256 of the weight files with the manifest and refuses |
| Steering hooks left installed after generation | generate raises |
apply_steering.py is byte-for-byte the file that was verified on a GPU, which is why the extra checks live
in a separate script. Run check_base_model.py once before you steer. The strongest check of all is
--verify: it regenerates 144 logged replies and compares their hashes.
How the vectors were made
- Data. ScriptShield corpus v0.2.0: 72 intent families, each with a harmful and a matched benign request, written in Urdu script, Roman Urdu, code-mixed Urdu-English and English. Families are split 24 calibration / 12 development / 36 test; no family crosses a split.
- Extraction. Lexsi SafeTune contrastive activation addition at the last prompt token:
mean(harmful) β mean(benign), separately for Urdu script and Roman Urdu on the calibration families;
pooledis their equal-weight mean. Cross-checked against an independent activation capture. - Script direction. Mean activation difference between Urdu-script and Roman-Urdu benign prompts.
residualized=pooledwith that direction projected out, rescaled to the pooled norm. - Selection. Layer and strength were chosen on the development families by a rule written down
beforehand (largest gain in harmful refusal with at most +2 points of benign refusal), on the
pooledarm only, and committed before the test set was run. - Evaluation. Generation and logging through Lexsi AuditKIT; data validation and export through Lexsi CuratorKIT.
Reproducibility
Verified in a fresh GPU session (Tesla T4, Tesla T4): this package alone (loader + vectors, no ScriptShield code importable) reproduced 144/144 logged outputs byte-for-byte (unsteered 48/48, pooled 48/48, residualized 48/48).
Test run produced at code commit 47413dfa41c32fe0f5b555ba1d64838d40317c67, base model revision 73500c80fcb6c8bea3e830c21d5d2920249a0e1a (the sha256 of
both weight shards and of the tokenizer are in manifest.json), float16 on a Tesla T4.
Versions
| Version | Date | What |
|---|---|---|
tag v0.2.0 |
2026-11-13 | first public release: vectors, manifest, loader and expected outputs exactly as verified on a GPU |
| current | 2026-11-13 | documentation rewritten; check_base_model.py added. The four verified files are unchanged |
sha256 of the four verified files (identical at v0.2.0 and now):
| File | sha256 |
|---|---|
steering_vectors.npz |
49bfdf61601aebd9e5cc974a3c8382ead4b415e68a1f9a353a3a3145179e2784 |
manifest.json |
f81ac10ee1d6b983f18db413186fe83b9387ddea07d13e621844a40b5229c400 |
apply_steering.py |
5b5e011b4a5d4976c7e4e7e0fc6d6997a34915eaf20ecf81fd258a0beee2f9c3 |
expected_outputs.jsonl |
216b04ecfe9b8e7d029c4f27a16adaa56137c0e1c478529868cf57e94b1a4fb4 |
To get exactly the first release: hf download ahmadrrrtx/scriptshield-tiny-aya-fire-steering --revision v0.2.0 --local-dir vectors.
Limitations
- Small, and not validated by annotators. 36 test families. The prompts were drafted with an LLM; a review by the project author (participant review, not independent expert validation) has been prepared and no prompt pair has been reviewed yet. Intervals are wide.
- The judge is unreliable in Roman Urdu. See the section above. The reading that corrects it was done by an LLM agent, not by people.
- One model, one layer, one strength, one seed, 64-token responses. Nothing here is known to transfer.
- Steering has a cost. On the development split, doubling the strength from Ο = 0.1 to 0.2 raised judged refusals of benign prompts by 8 to 17 points depending on the layer. At the frozen strength the judge-measured increase exceeded the pre-registered budget on the test set; on reading, those responses were garbled answers rather than refusals, which is a cost of a different kind.
- Not a defence. Anyone who controls the inference code can remove a steering vector.
How this was built
This project was built with substantial help from an AI assistant (an LLM agent). Under the author's direction it drafted the prompts, wrote much of the code and analysis, and did the response readings labelled above as agent readings. The author chose the research direction, approved the protocol and the release, and is responsible for the content. Responses were not labelled by human annotators; the author's review of the prompts is recorded in the dataset repository.
Licence and terms
The vectors are derived from the Tiny Aya weights and are released for non-commercial research under
CC-BY-NC 4.0; use is also subject to the Cohere Labs Acceptable Use Policy that governs the base model.
The base weights are not redistributed. apply_steering.py is also available under the MIT licence in the
code repository. SafeTune, AuditKIT and CuratorKIT are source-available under the Lexsi Labs licence (LSAL),
which has its own non-commercial terms.
Citation
@misc{scriptshield2026,
author = {Muhammad Ahmad},
title = {ScriptShield: script-dependent refusal in Tiny Aya Fire for Urdu and Roman Urdu},
year = {2026},
url = {https://github.com/ahmadrrrtx/scriptshield}
}
Acknowledgements
Built for the Lexsi hackathon with Lexsi Labs' CuratorKIT, SafeTune and AuditKIT. Base model by Cohere Labs. Judge models by the Qwen team (Apache-2.0).
