SafeKeep
July 31, 2026 · View on GitHub
Code for Tool Specifications Matter: Uncovering and Mitigating Safety Risks in AI Agents.
We identify schema-formatted tool specifications as a primary source of agent safety degradation and propose SafeKeep to restore native refusal behavior by assessing requests with flattened textual tool specifications before execution.
This repository contains the analysis that isolates the effect (Finding/) and the
method itself (Method/).
Layout
Dataset/
build_paired_direction_data.py build the paired harmful/harmless dataset
check_paired_data.py sanity checks over the released data
paired_direction_data/
paired_data.jsonl 400 prebuilt pairs (included)
Finding/
schema_to_prose.py schema neutralization (JSON -> flattened text)
semantic_to_gibberish.py semantic randomization (meaning removed, structure kept)
extract_refusal_direction.py per-layer refusal directions, chatbot vs agent
extract_schema_direction.py schema direction R_c (Eq. 2)
causal_intervention.py activation shifts at the peak refusal layer (Eq. 3, 4)
ablation/
input_ablation.py chatbot vs agent, representation level
behavior_ablation.py chatbot vs agent, behaviour level
component_ablation.py which part of the agent prompt causes the drop
tool_specification_ablation.py 2x2 over semantic content x schema elements
Method/
safekeep_demo.py end-to-end demo of the two SafeKeep stages
Setup
conda create -n safekeep python=3.11
conda activate safekeep
pip install -r requirements.txt
requirements.txt pins the versions used for the reported results. transformers in
particular is worth pinning: tool specifications are rendered by each model's own
chat template via apply_chat_template(tools=...), and that rendering is what the
analysis measures, so a different version changes the inputs themselves.
The last two entries (datasets, openai) are only needed to rebuild the dataset and
can be skipped otherwise.
Scripts load models from a local directory (local_files_only=True), so download the
weights first and pass the path with --model or set MODEL_PATH. Experiments in the
paper use Llama-3.1-8B-Instruct and Qwen2.5-7B-Instruct.
Two scripts call an external API and read credentials from the environment:
export LLM_JUDGE_API_KEY=... # behavior_ablation.py (refusal judge)
export OPENAI_API_KEY=... # build_paired_direction_data.py (dataset construction only)
Both accept an OPENAI_API_BASE / LLM_JUDGE_API_BASE override for OpenAI-compatible
endpoints.
Data
Dataset/paired_direction_data/paired_data.jsonl ships with the repository: 400 pairs
of harmful and harmless requests, each with the original system prompt containing the
tool schema.
{"harmful": "...", "harmless": "...", "system": "...you can invoke:\n[{...}]"}
Each harmless request is a minimal rewrite of its harmful counterpart — on average 7.2 of 21.5 words change — so the two differ in intent while the surrounding agent context stays fixed. Verify with:
python Dataset/check_paired_data.py
Rebuilding the data is only necessary if you want to regenerate or extend it, and additionally requires the ToolSafety dataset (Xie et al., 2025):
export TOOLSAFETY_PATH=/path/to/ToolSafety
python Dataset/build_paired_direction_data.py
Reproducing the analysis
Each script is standalone and fits its own directions on the train split, so they can be run in any order. Results are written under a directory named after the script, then the model.
Does the agent format weaken refusal? Representation level, then behaviour level:
python Finding/ablation/input_ablation.py --model $MODEL_PATH
python Finding/ablation/behavior_ablation.py --model $MODEL_PATH
Which part of the agent prompt is responsible? Components are added incrementally on top of the chatbot baseline; the 2x2 then separates semantic content from schema elements within the tool specification:
python Finding/ablation/component_ablation.py --model $MODEL_PATH
python Finding/ablation/tool_specification_ablation.py --model $MODEL_PATH
python Finding/ablation/tool_specification_ablation.py --make-table # after several models
Is the effect causal? Extract the directions, then intervene on activations:
python Finding/extract_refusal_direction.py --format chatbot --model $MODEL_PATH
python Finding/extract_refusal_direction.py --format agent --model $MODEL_PATH
python Finding/extract_schema_direction.py --model $MODEL_PATH
python Finding/causal_intervention.py --model $MODEL_PATH --mode restore --alphas 0,2,4,6,8
python Finding/causal_intervention.py --model $MODEL_PATH --mode subtract --alphas 0,4,8,12,16
Both transforms can be inspected on their own, without loading a model:
python Finding/schema_to_prose.py
python Finding/semantic_to_gibberish.py
Running SafeKeep
python Method/safekeep_demo.py --model $MODEL_PATH
Prints both stages for a harmful and a benign request that share the same tools and
phrasing: the neutralized tool view given to the judge, the verdict, and the resulting
response. --case harmful or --case benign runs just one.
Notes
- All experiments use a fixed 70/30 split with seed 42, shared across scripts, so directions fitted in one script are evaluated on inputs unseen in the same split.
- Hidden states are always taken at the final prompt token.
behavior_ablation.pycaches per-format results inresults_<format>.jsonand reuses them on rerun; delete that file to force regeneration.