fix(cron): sanitize invisible unicode in vetted skill content instead of hard-blocking#37245
Merged
Conversation
Contributor
🔎 Lint report:
|
… of hard-blocking A stray zero-width space (U+200B), BOM, or bidi control in loaded skill markdown permanently killed any cron that loaded it. The skills-attached assembled-prompt scan hard-blocked on any invisible-unicode char, even though skill bodies are already install-time vetted by skills_guard.py and the chars commonly appear in copy-pasted unicode docs / code examples. The skills path now strips invisibles (logging the codepoints) and runs the cleaned prompt. The raw user-prompt path (_scan_cron_prompt) keeps the hard block — that is the actual #3968 injection surface, where a small directive prompt with a ZWSP is a smoking gun, not prose. Stripping does not let a real injection slip through: the directive still matches after sanitization. _scan_cron_skill_assembled now returns (cleaned_prompt, error).
dc8d79b to
53be136
Compare
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
… of hard-blocking (NousResearch#37245) A stray zero-width space (U+200B), BOM, or bidi control in loaded skill markdown permanently killed any cron that loaded it. The skills-attached assembled-prompt scan hard-blocked on any invisible-unicode char, even though skill bodies are already install-time vetted by skills_guard.py and the chars commonly appear in copy-pasted unicode docs / code examples. The skills path now strips invisibles (logging the codepoints) and runs the cleaned prompt. The raw user-prompt path (_scan_cron_prompt) keeps the hard block — that is the actual NousResearch#3968 injection surface, where a small directive prompt with a ZWSP is a smoking gun, not prose. Stripping does not let a real injection slip through: the directive still matches after sanitization. _scan_cron_skill_assembled now returns (cleaned_prompt, error).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cron jobs that load a skill whose body contains a stray invisible-unicode character (zero-width space U+200B, BOM, bidi control) were permanently dead — the skills-attached prompt scan hard-blocked on any such char, even though skill bodies are already install-time vetted by
skills_guard.pyand these chars routinely appear in copy-pasted unicode docs / code examples.The skills path now strips invisibles (logging the codepoints) and runs the cleaned prompt. The raw user-prompt path keeps the hard block — that is the actual #3968 injection surface, where a tiny directive prompt with a ZWSP is a smoking gun, not prose.
Changes
tools/cronjob_tools.py: add_strip_invisible_unicode()(preserves emoji ZWJ);_scan_cron_skill_assembled()now sanitizes invisibles instead of blocking and returns(cleaned_prompt, error).cron/scheduler.py:_scan_assembled_cron_prompt()uses the cleaned prompt for the skills path; raw-prompt path unchanged (still hard-blocks).Why not the report's A/B/C re-architecture
The scan never touched the system prompt / memories / SOUL files — it only ever scanned
cron prompt + loaded skill content. No layer rework needed; the fix is scoped to the one path that false-positives.Validation
E2E: planted-skill
_build_job_prompt()run for all three cases — sanitized build, raw block, injection block all confirmed.Targeted suites:
tests/tools/test_cronjob_tools.py+tests/cron/test_cron_prompt_injection_skill.py→ 70/70 pass.Infographic