[BUG] security-guidance: yaml/torch/np.load rules stop at the first ")" — false positives with Loader=SafeLoader / weights_only=True after a nested call, missed np.load(..., allow_pickle=True)

Status Open
Reported on v2.1.289
Maintainer reply None cached
Activity 0 comments · opened Oct 5, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

Three rules in plugins/security-guidance/hooks/patterns.py (at 2bfb629) bound their look-ahead with [^)\n]{0,N}. That stops at the first ) on the line, and when an argument is itself a call, that ) closes the nested call, not the outer one. So the safe keyword after it is never seen:

| rule | pattern (patterns.py) |
|---|---|
| unsafe_yaml_load (:180) | \byaml\.load\s*\((?![^)\n]{0,80}\bSafe) |
| torch_unsafe_load (:244) | …torch\.load…\s*\((?![^)\n]{0,200}weights_only\s*=\s*True) |
| pickle_wrapper_load, numpy branch (:258) | \b(?:np\|numpy)\.load\s*\([^)\n]{0,200}allow_pickle\s*=\s*True |

The code's own comments describe the intent:

  • torch: "Suppressed by weights_only=True on the same line (within 200 chars)".
  • numpy: "numpy.load only triggers when allow_pickle=True is explicit".

Separately, \bSafe needs a word boundary, so Loader=yaml.CSafeLoader (libyaml's SafeLoader) is also flagged.

What Should Happen?

| line written by Edit | actual | expected |
|---|---|---|
| cfg = yaml.load(open(path), Loader=yaml.SafeLoader) | unsafe_yaml_load reminder ("can execute arbitrary Python") | no reminder |
| m = torch.load(os.path.join(d, "m.pt"), weights_only=True) | torch_unsafe_load reminder | no reminder |
| a = np.load(os.path.join(d, "x.npy"), allow_pickle=True) | no reminder | pickle_wrapper_load reminder |
| cfg = yaml.load(f, Loader=yaml.CSafeLoader) | unsafe_yaml_load reminder | no reminder |
| controls: np.load(path, allow_pickle=True) / torch.load(path, weights_only=True) | reminder / none | as is (correct) |

Steps to Reproduce

H=plugins/security-guidance/hooks/security_reminder_hook.py
S=$(mktemp -d); mkdir -p $S/state $S/proj; touch $S/state/.sdk_bootstrap_spawned
ed() { echo '{"session_id":"'$1'","hook_event_name":"PostToolUse","tool_name":"Edit","cwd":"'$S'/proj","tool_input":{"file_path":"'$S'/proj/'$1'.py","old_string":"x","new_string":"'"$2"'"}}' \
  | HOME=$S SECURITY_WARNINGS_STATE_DIR=$S/state CLAUDE_PROJECT_DIR=$S/proj python3 $H; echo; }
ed y1 'cfg = yaml.load(open(path), Loader=yaml.SafeLoader)'
ed t1 'm = torch.load(os.path.join(d, \"m.pt\"), weights_only=True)'
ed p1 'a = np.load(os.path.join(d, \"x.npy\"), allow_pickle=True)'
ed y2 'cfg = yaml.load(f, Loader=yaml.CSafeLoader)'

The output's metrics.rule_id is 12 for y1 and y2, 23 for t1, and the output is empty for p1.

Impact

  • False positives: yaml.load(open(p), Loader=SafeLoader) and torch.load(os.path.join(...), weights_only=True) are common, already-safe idioms. Each false reminder lands in Claude's context as an "arbitrary code execution" warning, which can push Claude into needless rewrites, and it is recorded as a pending warning.
  • Missed real sink: the np.load case is a real deserialization sink that gets no reminder.

Suggested fix

  • Scan to the end of the line, which is what the comments describe: replace [^)\n]{0,N} with [^\n]{0,N} in all three rules.
  • For yaml, accept the C loaders too, e.g. (?![^\n]{0,120}(?:\bC?Safe|safe_load)).
  • Add test cases with a nested call as the first argument.

Additional Information

  • Searched first: I searched issues and PRs for security-guidance + yaml/SafeLoader/torch/weights_only/np.load/allow_pickle false positive and found nothing. #55464 (eval rule on ast.literal_eval) and #61373 (exclude_substrings) are different.
  • Versions: Claude Code 2.1.289 (VS Code extension), Linux / WSL2. The bug is in the plugin's patterns, so platform doesn't matter.
  • Disclosure: found while auditing with AI tooling; reproduced by hand with the commands above.

View original on GitHub ↗