Skip to content

feat(add-new-block_code_execution-guardrail): prevent agent from executing code - #22056

Closed
ghost wants to merge 18 commits into
mainfrom
litellm_dev_02_23_2026_p2
Closed

feat(add-new-block_code_execution-guardrail): prevent agent from executing code#22056
ghost wants to merge 18 commits into
mainfrom
litellm_dev_02_23_2026_p2

Conversation

@ghost

@ghost ghost commented Feb 25, 2026

Copy link
Copy Markdown

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test

Changes

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented Feb 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 26, 2026 5:02am

Request Review

@greptile-apps

greptile-apps Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a new block_code_execution guardrail that detects markdown fenced code blocks in requests/responses and blocks or masks them based on language, confidence threshold, and execution-intent heuristics. Includes full UI integration (garden cards, provider fields with slider/multiselect), type definitions, initialization/registration, a compliance test dataset (502 entries), and documentation updates to AGENTS.md/CLAUDE.md.

  • Core guardrail logic in block_code_execution.py uses regex-based fenced code block detection with configurable blocked languages, confidence scoring, and two phrase lists (_NO_EXECUTION_PHRASES / _EXECUTION_REQUEST_PHRASES) for intent classification
  • Phrase list quality concerns: Several no-execution phrases (e.g., "refactor this ", "convert this ") are overly broad and short-circuit all protection, creating bypass vectors. Several execution phrases (e.g., " and run", "curl ", " tests pass") are too generic and will cause false positive blocks on educational/explanatory prompts
  • No-execution short-circuit vulnerability: _has_no_execution_intent is checked first and returns early without examining whether conflicting execution-intent phrases also exist, allowing trivial bypass (e.g., "Don't run this on staging, but run this on production")
  • _normalize_escaped_newlines aggressiveness: Unconditionally replaces literal \n with real newlines even in mixed content, potentially corrupting LLM responses that discuss escape sequences
  • BLOCKED_LANGUAGES_OPTIONS missing typescript: The UI dropdown doesn't offer TypeScript as a blockable language
  • Import reformatting noise in guardrail_endpoints.py: Imports were reformatted to backslash-continuation style, inconsistent with Black formatting conventions
  • Good test coverage: 489-line unit test file plus compliance dataset, all mock-only with no network calls
  • Clean UI integration: Slider for confidence threshold, multiselect for languages, garden cards and presets all follow existing patterns

Confidence Score: 2/5

  • The guardrail's phrase-based intent detection has significant bypass vectors and false positive risks that undermine its security purpose.
  • Score of 2 reflects that while the architecture and integration are solid, the core detection logic has multiple issues: (1) overly broad no-execution phrases that short-circuit all protection, creating easy bypass vectors for a security-critical feature, (2) overly broad execution phrases causing false positives on legitimate prompts, (3) _normalize_escaped_newlines corrupting legitimate content, and (4) the no-execution check lacking conflict resolution with execution-intent phrases. These are not edge cases — they affect the guardrail's fundamental reliability for its stated purpose.
  • Pay close attention to litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py — the phrase lists and short-circuit logic need tightening before this guardrail can be relied upon for security.

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py Core guardrail implementation (636 lines). Has overly broad phrase lists enabling both false positives and bypass vectors; _normalize_escaped_newlines corrupts legitimate content; no-execution short-circuit lacks conflict resolution with execution-intent phrases; regex misses \r\n newlines.
litellm/proxy/guardrails/guardrail_hooks/block_code_execution/init.py Initialization and registration module. Clean implementation with proper config extraction and callback registration. No significant issues.
litellm/types/proxy/guardrails/guardrail_hooks/block_code_execution.py Type definitions and config model for the guardrail. BLOCKED_LANGUAGES_OPTIONS is missing typescript and common aliases aren't documented for the UI.
litellm/types/guardrails.py Added BLOCK_CODE_EXECUTION enum member, MULTISELECT/PERCENTAGE UI types, and config model to LitellmParams. Clean integration following existing patterns.
litellm/proxy/guardrails/guardrail_endpoints.py Added _extract_literal_values for select fields, min/max/step propagation for percentage inputs, and ui_type string handling. Import reformatting is inconsistent with Black style.
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_block_code_execution.py Good test coverage (489 lines) with unit tests for detection, blocking, masking, escaped newlines, response-side blocking, and no-execution intent. All tests are mock-only with no network calls.
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_block_code_execution_compliance.py Compliance test runner against a 502-entry dataset. No network calls; validates 100% pass rate against the JSON dataset.
ui/litellm-dashboard/src/components/guardrails/guardrail_provider_fields.tsx Added Slider component for percentage-type fields with min/max/step marks. Clean integration with existing form rendering.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Request/Response Text] --> B[_normalize_escaped_newlines]
    B --> C{input_type?}
    C -->|request| D{_has_no_execution_intent?}
    C -->|response| F[_find_blocks - regex scan]
    D -->|yes| E[Return text unchanged - ALLOW]
    D -->|no| F
    F --> G{blocks found?}
    G -->|no, request| H{_has_execution_intent?}
    G -->|no, response| E
    G -->|yes| I[For each block: check language + confidence]
    H -->|yes, action=block| J[BLOCK - execution request]
    H -->|no| E
    I --> K{effective_block?}
    K -->|response| L[Always enforce block/mask]
    K -->|request + detect_intent| M{has_execution_intent?}
    K -->|request, no detect| L
    M -->|yes| L
    M -->|no| N[Log only / Allow]
    L --> O{action?}
    O -->|block| P[Raise HTTPException / ModifyResponseException]
    O -->|mask| Q[Replace with CODE_BLOCK_REDACTED]
Loading

Last reviewed commit: 490beb5

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 files reviewed, 18 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +267 to +333
@log_guardrail_information
async def apply_guardrail(
self,
inputs: GenericGuardrailAPIInputs,
request_data: dict,
input_type: Literal["request", "response"],
logging_obj: Optional["LiteLLMLoggingObj"] = None,
) -> GenericGuardrailAPIInputs:
start_time = datetime.now()
detections: List[CodeBlockDetection] = []
status: GuardrailStatus = "success"
exception_str = ""

try:
texts = inputs.get("texts", [])
if not texts:
return inputs

is_output = input_type == "response"
processed: List[str] = []
for text in texts:
new_text, should_raise = self._scan_text(text, detections)
processed.append(new_text)
if should_raise:
# Determine language from first blocking detection
lang = "unknown"
for d in detections:
if d.get("action_taken") == "block":
lang = d.get("language", "unknown")
break
self._raise_block_error(lang, is_output, request_data)

inputs["texts"] = processed
return inputs
except HTTPException:
status = "guardrail_intervened"
raise
except Exception as e:
status = "guardrail_failed_to_respond"
exception_str = str(e)
raise
finally:
guardrail_response: Union[List[dict], str] = [dict(d) for d in detections]
if status != "success" and not detections:
guardrail_response = exception_str
max_confidence: Optional[float] = None
for d in detections:
c = d.get("confidence")
if c is not None and (max_confidence is None or c > max_confidence):
max_confidence = c
tracing_kw: Dict[str, Any] = {
"guardrail_id": self.guardrail_name,
"detection_method": "fenced_code_block",
"match_details": guardrail_response,
}
if max_confidence is not None:
tracing_kw["confidence_score"] = max_confidence
self.add_standard_logging_guardrail_information_to_request_data(
guardrail_provider="block_code_execution",
guardrail_json_response=guardrail_response,
request_data=request_data,
guardrail_status=status,
start_time=start_time.timestamp(),
end_time=datetime.now().timestamp(),
duration=(datetime.now() - start_time).total_seconds(),
tracing_detail=GuardrailTracingDetail(**tracing_kw), # type: ignore[typeddict-item]
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double-logging of guardrail information

The @log_guardrail_information decorator already calls self._process_response() on success and self._process_error() on exception — both of which internally call self.add_standard_logging_guardrail_information_to_request_data(). The finally block at line 308 also calls self.add_standard_logging_guardrail_information_to_request_data() directly, causing guardrail information to be appended to request_data["metadata"]["standard_logging_guardrail_information"] twice per invocation.

Other guardrails in the codebase use one pattern or the other, but never both:

  • Decorator-only (e.g. PresidioPIIMasking.apply_guardrail): uses @log_guardrail_information and does not manually call add_standard_logging_guardrail_information_to_request_data.
  • Manual-only (e.g. ContentFilterGuardrail.apply_guardrail): does NOT use @log_guardrail_information and manually calls add_standard_logging_guardrail_information_to_request_data in finally.

Either remove the @log_guardrail_information decorator and keep the manual finally block logging, or remove the finally block and rely on the decorator.

Comment on lines +335 to +362
async def async_post_call_streaming_iterator_hook(
self,
user_api_key_dict: Any,
response: Any,
request_data: dict,
) -> AsyncGenerator[ModelResponseStream, None]:
"""Accumulate streamed content and block if a complete fenced code block is detected."""
accumulated = ""
async for item in response:
if isinstance(item, ModelResponseStream) and item.choices:
delta_content = ""
is_final = False
for choice in item.choices:
if hasattr(choice, "delta") and choice.delta:
content = getattr(choice.delta, "content", None)
if content and isinstance(content, str):
delta_content += content
if getattr(choice, "finish_reason", None):
is_final = True
accumulated += delta_content
if is_final:
# Run detection on full accumulated text (streaming: block only, no mask)
blocks = self._find_blocks(accumulated)
for _tag, _body, confidence, action_taken in blocks:
if action_taken == "block" and confidence >= self.confidence_threshold:
lang = _tag or "unknown"
self._raise_block_error(lang, True, request_data)
yield item

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streaming hook yields blocked content before raising error

async_post_call_streaming_iterator_hook yields every chunk immediately (yield item at line 362) and only checks for blocked code blocks when is_final is True. This means all streamed chunks containing the blocked code are already sent to the client before the error is raised. By the time the guardrail detects the blocked code at end-of-stream, the content has already been yielded.

To actually prevent the client from receiving blocked content during streaming, the hook would need to buffer chunks and only yield them after confirming they don't contain (or complete) a blocked code block — or at minimum, not yield the final chunk and raise before it.

Comment on lines +188 to +246
def _scan_text(
self,
text: str,
detections: Optional[List[CodeBlockDetection]] = None,
) -> Tuple[str, bool]:
"""
Scan one text: find blocks, apply block/mask/allow by confidence.
Returns (modified_text, should_raise).
"""
if not text:
return text, False
blocks = self._find_blocks(text)
if not blocks:
return text, False

should_raise = False
last_end = 0
parts: List[str] = []
for m in FENCED_BLOCK_RE.finditer(text):
tag = (m.group(1) or "").strip()
tag_in_list = not self.block_all and _normalize_language(tag) in [
_normalize_language(t) for t in (self.blocked_languages or [])
]
is_blocked = _is_blocked_language(
tag, self.blocked_languages, self.block_all
)
confidence = _confidence_for_block(tag, self.block_all, tag_in_list)
if not is_blocked:
action_taken: CodeBlockActionTaken = "allow"
elif confidence >= self.confidence_threshold:
action_taken = "block"
else:
action_taken = "log_only"

if detections is not None:
detections.append(
cast(
CodeBlockDetection,
{
"type": "code_block",
"language": tag or "(none)",
"confidence": round(confidence, 2),
"action_taken": action_taken,
},
)
)

if action_taken == "block" and self.action == "block":
should_raise = True
parts.append(text[last_end : m.start()])
if action_taken == "block":
parts.append(self.MASK_PLACEHOLDER)
else:
parts.append(text[m.start() : m.end()])
last_end = m.end()

parts.append(text[last_end:])
new_text = "".join(parts)
return new_text, should_raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant double regex iteration in _scan_text

_scan_text calls self._find_blocks(text) at line 199, which iterates over all FENCED_BLOCK_RE matches and computes tag_in_list, is_blocked, confidence, and action_taken for each match. Then at line 206 the method iterates FENCED_BLOCK_RE.finditer(text) again and recomputes the exact same values. The blocks result from _find_blocks is only used as an early-return check (if not blocks).

Consider removing the _find_blocks call and using the inline regex iteration directly, or reusing the results from _find_blocks instead of running the regex a second time.

@ghost

ghost commented Feb 25, 2026

Copy link
Copy Markdown
Author

@greptile please re-review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +30 to +45
DEFAULT_BLOCKED_LANGUAGES: List[str] = [
"python",
"javascript",
"js",
"bash",
"sh",
"ruby",
"go",
"java",
"csharp",
"php",
"c",
"cpp",
"rust",
"sql",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEFAULT_BLOCKED_LANGUAGES is defined but never used

The DEFAULT_BLOCKED_LANGUAGES list (lines 30-45) is never referenced anywhere in the codebase. When blocked_languages is None or empty, the guardrail uses block_all = True mode rather than falling back to this list. Consider removing this dead code or using it as the default for BLOCKED_LANGUAGES_OPTIONS in the config model to keep a single source of truth.

…block_code_execution.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@ghost

ghost commented Feb 25, 2026

Copy link
Copy Markdown
Author

@greptile please re-review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +320 to +322
except ModifyResponseException:
status = "guardrail_intervened"
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import causes NameError at runtime

ModifyResponseException is caught at line 320 but is never imported in this file. When input_type == "request", _raise_block_error calls self.raise_passthrough_exception() which raises ModifyResponseException. Since the name is not in scope, Python will raise a NameError instead, which falls into the generic except Exception block at line 323 — misclassifying the intentional guardrail intervention as "guardrail_failed_to_respond".

The test file (test_block_code_execution.py:6) correctly imports it, but the implementation file does not. Add the import alongside CustomGuardrail:

Suggested change
except ModifyResponseException:
status = "guardrail_intervened"
raise
except ModifyResponseException:
status = "guardrail_intervened"
raise

Also add at the top of the file (line 16):

from litellm.integrations.custom_guardrail import CustomGuardrail, ModifyResponseException

Comment on lines +83 to +96
def _is_blocked_language(
tag: str,
blocked_languages: Optional[List[str]],
block_all: bool,
) -> bool:
"""True if this language tag should be considered blocked."""
normalized = _normalize_language(tag)
if block_all:
# Block all: only allow through if it's explicitly non-executable (we still block but with lower confidence)
return True
if not blocked_languages:
return True
normalized_list = [_normalize_language(t) for t in blocked_languages]
return normalized in normalized_list

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable code path when block_all=False and blocked_languages is empty

When block_all is False, the blocked_languages list is guaranteed to be non-empty (because block_all is set to True when blocked_languages is None or len(blocked_languages) == 0 in __init__ at line 166). This means the check at line 93 (if not blocked_languages: return True) is dead code — it can never be reached when block_all is False.

This isn't a runtime bug, but it makes the logic harder to reason about. Consider removing the dead branch or adding a comment explaining the invariant.

Comment on lines +63 to +74
def _normalize_escaped_newlines(text: str) -> str:
"""
Replace literal escaped newlines (backslash + n or backslash + r) with real newlines.
API/JSON payloads sometimes deliver newlines as the two-character sequence \\n.
"""
if not text:
return text
# Order matters: replace \r\n first so we don't produce extra \n from \r then \n
text = text.replace("\\r\\n", "\n")
text = text.replace("\\n", "\n")
text = text.replace("\\r", "\n")
return text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_normalize_escaped_newlines may corrupt legitimate content

This function unconditionally replaces the literal two-character sequence \n with a real newline in all text. This is problematic because:

  1. LLM responses can legitimately contain the literal string \n when discussing code, escape sequences, or regex patterns (e.g., explaining "use \\n for newlines")
  2. JSON-decoded strings would already have real newlines — the double-escaped \\n → literal \n scenario only occurs when text was not properly decoded from JSON

Consider applying this normalization more selectively — for example, only when the text appears to not contain any real newlines but does contain literal \n sequences, which would indicate it was passed as a raw JSON-encoded string.

@ghost

ghost commented Feb 25, 2026

Copy link
Copy Markdown
Author

@greptile can you please re-review?

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +357 to +361
for _start, _end, _tag, _body, confidence, action_taken in blocks:
if (
action_taken == "block"
and confidence >= self.confidence_threshold
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant confidence check in streaming hook

The confidence >= self.confidence_threshold check here is redundant. _find_blocks at line 188 already ensures that action_taken is set to "block" only when confidence >= self.confidence_threshold. If action_taken == "block", the confidence condition is guaranteed to be true.

Suggested change
for _start, _end, _tag, _body, confidence, action_taken in blocks:
if (
action_taken == "block"
and confidence >= self.confidence_threshold
):
if action_taken == "block":

Comment on lines +54 to +55
if "\n" in text:
return text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed real/escaped newlines bypass detection

If text contains both real newlines and literal \n sequences (e.g., "line1\nline2\\nline3"), this early return skips normalization entirely. A code block that uses literal \n within otherwise normal text would not be detected. Consider removing this early return or changing the heuristic to only skip when there are no \\n sequences present.

Suggested change
if "\n" in text:
return text
if "\n" in text and "\\n" not in text:
return text

Comment on lines +354 to +356
accumulated += delta_content
# Check after every chunk so we block before yielding the chunk that completes a blocked block
blocks = self._find_blocks(accumulated)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streaming hook skips escaped newline normalization

_scan_text applies _normalize_escaped_newlines before calling _find_blocks, but the streaming hook calls _find_blocks directly on the raw accumulated text. If a streaming response contains literal \n sequences (e.g., from a JSON-encoded payload), the regex won't match and the code block won't be detected.

Consider normalizing before the check:

                normalized = _normalize_escaped_newlines(accumulated)
                blocks = self._find_blocks(normalized)

@greptile-apps

greptile-apps Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

litellm/proxy/guardrails/guardrail_endpoints.py
Redundant and unused inline imports

concurrent.futures is already imported at line 5 and get_custom_code_primitives is already imported at lines 23-25. Additionally, re is imported here but never used anywhere in this function.

@ghost

ghost commented Feb 25, 2026

Copy link
Copy Markdown
Author

@greptile please re-review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +30 to +34
# Language tag aliases (normalize to canonical for comparison)
LANGUAGE_ALIASES: Dict[str, str] = {
"js": "javascript",
"py": "python",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing sh/bash and ts/typescript aliases causes detection gaps

LANGUAGE_ALIASES only maps js → javascript and py → python, but BLOCKED_LANGUAGES_OPTIONS in the config model offers both "bash" and "sh" as separate options. Since "sh" is not aliased to "bash" (or vice versa), a user who selects only "bash" will not block ```sh code blocks, and vice versa. The same gap exists for typescript/ts.

For example, if blocked_languages = ["bash"] and the LLM returns ```sh\nrm -rf /\n```, _normalize_language("sh") returns "sh" which is not in ["bash"] — so the block is allowed through.

Suggested change
# Language tag aliases (normalize to canonical for comparison)
LANGUAGE_ALIASES: Dict[str, str] = {
"js": "javascript",
"py": "python",
}
LANGUAGE_ALIASES: Dict[str, str] = {
"js": "javascript",
"py": "python",
"sh": "bash",
"ts": "typescript",
}

Comment on lines +336 to +363
async def async_post_call_streaming_iterator_hook(
self,
user_api_key_dict: Any,
response: Any,
request_data: dict,
) -> AsyncGenerator[ModelResponseStream, None]:
"""Accumulate streamed content and block as soon as a complete fenced code block is detected (before yielding that chunk)."""
accumulated = ""
async for item in response:
if isinstance(item, ModelResponseStream) and item.choices:
delta_content = ""
for choice in item.choices:
if hasattr(choice, "delta") and choice.delta:
content = getattr(choice.delta, "content", None)
if content and isinstance(content, str):
delta_content += content
accumulated += delta_content
# Check after every chunk so we block before yielding the chunk that completes a blocked block
normalized = _normalize_escaped_newlines(accumulated)
blocks = self._find_blocks(normalized)
for _start, _end, _tag, _body, confidence, action_taken in blocks:
if (
action_taken == "block"
and confidence >= self.confidence_threshold
):
lang = _tag or "unknown"
self._raise_block_error(lang, True, request_data)
yield item

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streaming hook does not log guardrail information

Unlike apply_guardrail which has a finally block that calls self.add_standard_logging_guardrail_information_to_request_data(...), the streaming hook never logs guardrail tracing data. When the guardrail blocks during streaming (raising HTTPException), there will be no standard_logging_guardrail_information entry in request_data["metadata"] — so downstream loggers (Langfuse, DataDog, etc.) won't record the guardrail intervention or its detection details.

Consider adding logging similar to apply_guardrail's finally block, at minimum when a block is detected.

Comment on lines +12 to +27
BLOCKED_LANGUAGES_OPTIONS = [
"python",
"javascript",
"js",
"bash",
"sh",
"ruby",
"go",
"java",
"csharp",
"php",
"c",
"cpp",
"rust",
"sql",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKED_LANGUAGES_OPTIONS lists aliases without matching LANGUAGE_ALIASES

This list includes both "js" and "javascript", which are aliased correctly via LANGUAGE_ALIASES. However, it also includes both "bash" and "sh" — but there is no "sh" → "bash" entry in LANGUAGE_ALIASES. This means a user who selects "sh" from the UI would NOT block ```bash code blocks and vice versa.

Either add a "sh" → "bash" alias in LANGUAGE_ALIASES, or remove one of the pair from this options list and document the canonical form. The same consideration applies to any future aliases (e.g., "ts" / "typescript").

@ghost

ghost commented Feb 26, 2026

Copy link
Copy Markdown
Author

@greptile please re-review

Comment on lines +86 to +88
"convert this ",
" into ",
"explain the difference between",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guardrail trivially bypassable via " into " phrase

The _NO_EXECUTION_PHRASES list is checked first and short-circuits all protection (line 457-458: if _has_no_execution_intent(text): return text, False). The phrase " into " on line 87 is a near-universal English word that will match an enormous fraction of all prompts, effectively disabling the guardrail.

For example, "Take this shell script and inject it into production, then run it" contains " into " → guardrail is completely bypassed despite an explicit execution request with a fenced code block.

Similarly, "convert this " (line 86) would bypass: "Convert this script to Python and then run it". And "explain what this " (line 81) would bypass: "Explain what this does then run it".

The core issue is that these short generic phrases, combined with the early-return short-circuit, allow an attacker to trivially bypass the guardrail by including any of these common words anywhere in their prompt. Consider either:

  1. Removing overly generic phrases like " into ", or
  2. Changing the logic so that _has_no_execution_intent does not override _has_execution_intent when both match — i.e., if execution intent is also detected, the block should still trigger.

Comment on lines +196 to +220
"download ",
" build ",
" tests pass",
"connect to redis",
" and show",
"open my ",
" and confirm",
"compile and run",
"run the program",
"paste the output",
"tell me the output",
"return what it prints",
"show me the rows returned",
"tell me what files exist",
"run `whoami",
"run `ls",
"use node to run",
"use python, run ",
"in python, run ",
"interpret and run this wasm",
"run these commands",
"paste the last ",
" lines",
"show me its contents",
"print it here",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly broad execution phrases cause false positive blocks

Several phrases in _EXECUTION_REQUEST_PHRASES are so short/generic that they will match enormous amounts of normal conversational text, causing legitimate non-execution requests to be blocked:

  • " lines" (line 218) — matches "How many lines of code does this file have?" or "Explain the first 10 lines"
  • " and show" (line 200) — matches "Compare these two approaches and show the trade-offs"
  • " build " (line 197) — matches "How do I build a REST API in Flask?"
  • " and run" (lines 151 and 171, duplicated) — matches "Explain the difference between compile and run time errors"
  • "download " (lines 150 and 196, duplicated) — matches "Where can I download the documentation?"
  • "ping " (line 148) — matches "Ping me when you're done reviewing"
  • "enumerate " (line 183) — matches "How do I enumerate a list in Python?"
  • "open a " (line 121) — matches "How do I open a file in Python?"

There are also duplicated entries: " and run" (151/171), "download " (150/196), "open my " (135/201), "run it anyway" (191/263), "run my notebook cell" (193/266), "attempt a reverse shell" (182/252), "paste the output" (205/233).

Since all matching is done via simple in substring containment, there are no word-boundary checks. Consider using regex with \b boundaries, removing the most generic phrases, or requiring multiple signals to trigger.

Comment on lines +293 to +305
def _normalize_escaped_newlines(text: str) -> str:
"""
Replace literal escaped newlines (backslash + n or backslash + r) with real newlines.
API/JSON payloads sometimes deliver newlines as the two-character sequence \\n.
Applied whenever \\n or \\r appear, including in mixed content with real newlines.
"""
if not text:
return text
if "\\n" not in text and "\\r" not in text:
return text
# Order matters: replace \r\n first so we don't produce extra \n from \r then \n
text = text.replace("\\r\\n", "\n")
text = text.replace("\\n", "\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_normalize_escaped_newlines corrupts legitimate \n literals

This function unconditionally replaces the literal two-character sequence \n with a real newline. LLM responses frequently discuss escape sequences and contain the literal string \n in explanatory text (e.g., "use \\n for newlines in Python"). After this normalization, such text will have its content altered — potentially breaking code explanations and creating phantom code block matches where fences were not intended.

For example, the text "In Python, print('hello\\nworld') outputs two lines" would be transformed to "In Python, print('hello\nworld') outputs two lines" — changing the semantic content of the response.

Consider applying this normalization more selectively — for example, only when the text contains no real newlines at all (suggesting it was passed as a raw JSON-encoded string), rather than always applying it.

Comment thread ui/litellm-dashboard/tsconfig.json Outdated
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx: "preserve" may break build pipeline

Changing jsx from "react-jsx" to "preserve" means TypeScript will no longer transform JSX — it will be emitted as-is and must be handled by a downstream tool (e.g., Babel, SWC). If the existing Next.js build pipeline was configured to rely on TypeScript's JSX transformation, this change could cause build failures. Was this change intentional? If this is a Next.js project using SWC (the default), "preserve" is typically correct, but the change should be tested against the full build.

Suggested change
"jsx": "preserve",
"jsx": "preserve",

@ghost

ghost commented Feb 26, 2026

Copy link
Copy Markdown
Author

@greptile please review

Comment on lines +468 to +471
for start, end, tag, _body, confidence, action_taken in blocks:
effective_block = action_taken == "block" and (
not self.detect_execution_intent or has_execution_intent
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response-side guardrail silently bypassed by default

When detect_execution_intent=True (the default), effective_block at line 469-471 requires has_execution_intent to be True. But has_execution_intent calls _has_execution_intent(text) which checks for phrases like "run this", "execute this", etc. — phrases that appear in user requests, not in LLM responses.

This means when apply_guardrail is called with input_type="response", the LLM response text (e.g. "Here is a Python script:\n```python\nimport os; os.system('rm -rf /')\n```") will almost never contain execution intent phrases, so has_execution_intent will be False, effective_block will be False, and the code block passes through unblocked.

Since the default event_hook includes both pre_call and post_call, and detect_execution_intent defaults to True, response-side blocking is effectively disabled out of the box.

The fix should skip the execution-intent check when scanning responses (since it's the LLM's output, not a user request):

Suggested change
for start, end, tag, _body, confidence, action_taken in blocks:
effective_block = action_taken == "block" and (
not self.detect_execution_intent or has_execution_intent
)
effective_block = action_taken == "block" and (
not self.detect_execution_intent or has_execution_intent or input_type == "response"
)

Note: this requires threading input_type through to _scan_text, or moving the effective_block logic into apply_guardrail where input_type is available.

Comment on lines +440 to +441
if self.detect_execution_intent and _has_no_execution_intent(text):
return text, False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-execution short-circuit also disables response-side blocking

Same issue as the effective_block bug: when detect_execution_intent=True and this is called for a response, _has_no_execution_intent checks whether the LLM's output text contains phrases like "explain what this " or "can you explain". If the LLM response happens to contain any of these common phrases (e.g. "I can explain what this code does..." contains "can you explain" substring via "can explain"), the guardrail short-circuits and returns the text unmodified — even if it contains dangerous executable code blocks.

This check should only apply when scanning requests, not responses.

"just reason",
"explain without running",
"explain without execute",
"what would ",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly broad no-execution phrase enables guardrail bypass

The phrase "what would " (with trailing space) is extremely common in natural language. It will match prompts like:

  • "What would happen if I run this malicious code on your system?" — adversarial, but guardrail is bypassed
  • "What would be the best way to execute this script?" — execution request, but bypassed

Because _has_no_execution_intent is checked first and short-circuits (line 440-441), any prompt containing "what would " will bypass the guardrail entirely, regardless of whether it also contains execution-intent phrases.

Similarly, "can you explain" (line 106) matches "Can you explain how to run this, then run it?" and "explain what this " (line 81) matches "Explain what this does then execute it".

Consider making these phrases more specific, or require that no execution-intent phrases are also present (i.e., check both lists and resolve conflicts rather than short-circuiting on the no-execution list).

…tighten no-execution phrases (#22149)

**Core bug fix**: Response-side blocking was silently disabled with detect_execution_intent=True (default) because execution-intent heuristics were applied to LLM output text, which doesn't contain phrases like 'run this'. Now input_type is threaded through _scan_text to skip intent checks for responses while still blocking detected code blocks.

**Tightened overly broad no-execution phrases**: Replaced broad patterns like "what would ", "can you explain", and "explain what this " with more specific forms (e.g. "what would happen if", "can you explain this code") to prevent trivial bypass.

**Added tests**: 7 new test cases covering response-side blocking with default settings, casual phrases in LLM output, and tightened phrase patterns. All 23 tests pass + 100% compliance dataset compliance (100/100).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@ghost

ghost commented Feb 26, 2026

Copy link
Copy Markdown
Author

@greptile please re-review this

Comment on lines 17 to +37
from litellm.proxy.guardrails.guardrail_hooks.custom_code.code_validator import (
CustomCodeValidationError,
validate_custom_code,
)
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import (
get_custom_code_primitives,
)
from litellm.proxy.guardrails.usage_endpoints import router as guardrails_usage_router
from litellm.types.guardrails import (
PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BaseLitellmParams,
BedrockGuardrailConfigModel,
Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
GuardrailUIAddGuardrailSettings,
LakeraV2GuardrailConfigModel,
ListGuardrailsResponse,
LitellmParams,
PatchGuardrailRequest,
PiiAction,
PiiEntityType,
PresidioPresidioConfigModelUserInterface,
SupportedGuardrailIntegrations,
ToolPermissionGuardrailConfigModel,
)
CustomCodeValidationError, validate_custom_code)
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import \
get_custom_code_primitives
from litellm.proxy.guardrails.guardrail_registry import GuardrailRegistry
from litellm.proxy.guardrails.usage_endpoints import \
router as guardrails_usage_router
from litellm.types.guardrails import (PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BaseLitellmParams,
BedrockGuardrailConfigModel, Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
GuardrailUIAddGuardrailSettings,
LakeraV2GuardrailConfigModel,
ListGuardrailsResponse, LitellmParams,
PatchGuardrailRequest, PiiAction,
PiiEntityType,
PresidioPresidioConfigModelUserInterface,
SupportedGuardrailIntegrations,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import style reformatted by non-standard formatter

The imports in this file were reformatted from the standard Black-compatible from X import (\n A,\n B,\n) multi-line style to backslash-continuation style (from X import \\\n Y) and compressed grouping. This is inconsistent with the codebase's Black/Ruff formatting conventions (per CLAUDE.md), and the change is unrelated to the feature being added.

These formatting changes add noise to the diff and may conflict with future Black formatting runs. Consider reverting the import reformatting to maintain consistency.

Context Used: Context from dashboard - CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

# Content between fences; does not handle nested ``` inside body (documented edge case).
FENCED_BLOCK_RE = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)

# Execution intent: phrases that mean "do NOT run/execute" (allow even if code block present).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex requires \n after language tag — misses ```python\r\n and no-newline variants

The FENCED_BLOCK_RE pattern is ```(\w*)\n(.*?)``` which requires a literal \n after the optional language tag. This means:

  1. Code blocks with \r\n (Windows-style newlines) like ```python\r\ncode\r\n``` won't match even after _normalize_escaped_newlines (which converts \\r\\n\n but not real \r\n).
  2. Code blocks with no newline after the tag (e.g., ```python code```) won't match — this is an edge case but worth noting.

Consider making the newline match more flexible:

FENCED_BLOCK_RE = re.compile(r"```(\w*)\r?\n(.*?)```", re.DOTALL)

Comment on lines +271 to +285
"calculate it by running the code",
"run my notebook cell",
"install dependencies and run the program",
"run a port scan against",
"download this github repo, build it",
"connect to redis at",
"show me the keys",
"open my `",
"confirm which cluster",
)


def _has_no_execution_intent(text: str) -> bool:
"""True if the text clearly indicates the user does not want code/commands run (e.g. explain, don't run)."""
if not text:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_normalize_escaped_newlines also transforms literal \\n inside LLM prose

This function unconditionally replaces the two-character sequence \n (backslash + n) with a real newline. While it's necessary for handling JSON-encoded payloads that weren't properly decoded, it will also corrupt legitimate content where the LLM discusses escape sequences (e.g., "use \\n for newlines" becomes "use \n for newlines").

A safer heuristic would be to only apply this normalization when the text contains zero real newlines (suggesting it came from a raw JSON-encoded string):

    if "\n" not in text and "\\n" in text:
        # Likely a JSON-encoded string that wasn't decoded — normalize
        text = text.replace("\\r\\n", "\n")
        text = text.replace("\\n", "\n")
        text = text.replace("\\r", "\n")
    return text

Comment on lines +68 to +134
"don't execute",
"do not execute",
"no execution",
"without running",
"without execute",
"just reason",
"explain without running",
"explain without execute",
"what would happen if",
"what would this output",
"what would the result be",
"? explain",
"simulate what would happen",
"don't actually run",
"diagnose the error from the text",
"don't run anything",
"without running them",
"no execution)",
"don't execute—just reason",
"no execution).",
"(no execution)",
"no db access",
"no db access).",
"don't execute it",
"don't run).",
"(no execution)",
"no builds/run",
"(don't run)",
"no execution).",
"but don't run",
"don't run it",
"explain what this code",
"explain what this script",
"explain what this function",
"explain what this sql",
"refactor this ",
"spot any security issues",
"write unit tests for this function without running",
"what output *should* this produce",
"convert this ",
"explain the difference between",
"given this stack trace, explain",
"write a safe alternative",
"write a python function",
"generate a dockerfile",
"write a bash script that would",
"create a minimal ",
" example (no execution)",
"write pseudocode",
"generate typescript types",
"write a safe wrapper",
"show how to parse stdout",
"can you *simulate*",
"is this command safe to run",
"i pasted logs from",
"can you diagnose",
"what would `git",
"here's a traceback",
"can you explain this code",
"can you explain what this",
"can you explain how this works",
)

# Execution intent: phrases that mean "run/execute/perform this for me" (block when on request).
# Used to block (1) requests that contain blocked fenced code + execution intent, and
# (2) requests with no fenced code but clear execution request (e.g. "run `ls`", "read /etc/passwd").
_EXECUTION_REQUEST_PHRASES: Tuple[str, ...] = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_NO_EXECUTION_PHRASES contains overly broad and duplicate entries

Several entries in this list are problematic:

  1. Overly broad: "refactor this " (line 103), "convert this " (line 106), "create a minimal " (line 113) are very short generic phrases that can appear in prompts with execution intent (e.g., "Refactor this and then run it", "Convert this to Python and execute it"). Since these short-circuit all blocking via _has_no_execution_intent, they create easy bypass vectors.

  2. Duplicate entries: "(no execution)" appears at lines 91, 95, and 98. "no execution)." appears at lines 92 and 100.

  3. Trailing punctuation inconsistency: Some entries include trailing punctuation like "no execution).", "don't run).", "no db access)." which will only match when the exact punctuation is present.

Consider tightening broad phrases to require more specific context (e.g., "refactor this code" instead of "refactor this ") and removing duplicates.

Comment on lines +139 to +262
"can you run ",
"run `",
"execute `",
" repl ",
"run this python",
"run this javascript",
"run this bash",
"run this node",
"run this sql",
"run this c ",
"run this powershell",
"run this ruby",
"run this lua",
"read `/",
"read /",
"open `",
"open my ",
"list the files",
"create a file",
"search my disk",
"cat the contents",
"write a script, execute it",
"check whether ",
" exists and paste",
"load `",
" and compute",
"curl ",
"make an http request",
"run `nslookup",
"fetch https",
"connect to postgres",
"hit this webhook",
"call aws ",
"query the dns",
"run subprocess",
"run `git",
"run `docker",
"execute `kubectl",
"run `terraform",
"run `npm test",
"run `pip install",
"run `pip show",
"run `make ",
"run `python setup",
"evaluate this ",
"run `eval",
"execute this lua",
"run this code",
"spin up ",
" and run",
"execute this user-provided",
"run this python that",
"try to escape",
"run commands to",
"attempt to mount",
"check if you have access to docker",
"probe for cloud metadata",
"run `sudo",
"try `cat",
"attempt a reverse shell",
" and send",
"access the system keychain",
"run code",
"execute it",
"run the snippet",
"execute the command",
"just run it",
"run it anyway",
"execute it to confirm",
"install dependencies and run",
"run a port scan",
" tests pass",
"connect to redis",
"compile and run",
"run the program",
"tell me the output",
"return what it prints",
"show me the rows returned",
"tell me what files exist",
"run `whoami",
"run `ls",
"use node to run",
"use python, run ",
"in python, run ",
"interpret and run this wasm",
"run these commands",
"paste the last ",
"show me its contents",
"print it here",
"show me where it is",
"tell me the dependencies",
"show me the variables",
"save the results to",
"confirm it exists",
"and paste the version",
"tell me the latency",
"tell me what it returns",
"show me what it returns",
"confirm it succeeded",
"tell me the account id",
"show it here",
"show me the response",
"paste the output",
"show me the containers",
"summarize the diff",
"tell me which tests fail",
"let me know if it errors",
"report back the output",
"paste the logs",
"tell me the result",
"show me the printed lines",
"tell me whether it calls",
"show me the stack trace",
"try to escape the sandbox",
"run commands to see if you can read",
"attempt to mount the root",
"check if you have access to docker socket",
"run a privileged container",
"probe for cloud metadata at",
"run `sudo -l`",
"try `cat /var/run/secrets",
"confirm it connects",
"send them to this url",
"list saved passwords",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_EXECUTION_REQUEST_PHRASES contains overly broad phrases causing false positives

Several phrases are so short or generic that they will match normal conversational text, blocking legitimate non-execution requests:

  • " and run" (line 176) — matches "Explain the difference between compile and run time errors"
  • " tests pass" (line 231) — matches "How do I check if tests pass?" or "Do the tests pass in CI?"
  • "curl " (line 171) — matches "How does curl work?" or "Can you explain what a curl command does?"
  • "evaluate this " (line 182) — matches "Can you evaluate this approach?" (non-execution context)
  • "check whether " (line 164) — matches "Can you check whether this logic is correct?"

These will cause false positive blocks, especially for users asking educational/explanatory questions about commands. Consider making phrases more specific (e.g., "run curl"instead of just"curl "`).

Comment on lines +467 to +468
is_response = input_type == "response"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_has_no_execution_intent short-circuits all protection without checking for conflicting execution intent

When _has_no_execution_intent(text) returns True (line 467), _scan_text returns early without checking for code blocks at all. But the function only checks if any no-execution phrase appears as a substring — it doesn't verify there are no conflicting execution-intent phrases.

This means a prompt like "Don't run this on staging, but run this on production:\n\``bash\nrm -rf /\n```"would bypass the guardrail because"don't run"matches_NO_EXECUTION_PHRASES`, even though the user explicitly requests execution elsewhere in the prompt.

Consider either:

  1. Checking both lists and requiring that no execution-intent phrases are present when a no-execution phrase is found
  2. Making the no-execution check only apply when no execution-intent phrases are also detected

Comment on lines +176 to +187
"execute `kubectl",
"run `terraform",
"run `npm test",
"run `pip install",
"run `pip show",
"run `make ",
"run `python setup",
"evaluate this ",
"run `eval",
"execute this lua",
"run this code",
"spin up ",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate " and run" and "download " entries

" and run" appears at both lines 176 and 199. "download " appears at both lines 150 and 220. These duplicates don't cause runtime errors but should be cleaned up:

Suggested change
"execute `kubectl",
"run `terraform",
"run `npm test",
"run `pip install",
"run `pip show",
"run `make ",
"run `python setup",
"evaluate this ",
"run `eval",
"execute this lua",
"run this code",
"spin up ",
" and run",
"execute this user-provided",
"run this python that",
"try to escape",
"run commands to",
"attempt to mount",
"check if you have access to docker",
"probe for cloud metadata",
"run `sudo",
"try `cat",
"attempt a reverse shell",
"access the system keychain",

Comment on lines +14 to +27
BLOCKED_LANGUAGES_OPTIONS = [
"python",
"javascript",
"bash",
"ruby",
"go",
"java",
"csharp",
"php",
"c",
"cpp",
"rust",
"sql",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKED_LANGUAGES_OPTIONS missing common aliases like sh, ts, js, py

This list only includes canonical names (e.g., "python", "bash"), but LANGUAGE_ALIASES in the guardrail normalizes sh→bash, ts→typescript, js→javascript, py→python. However, BLOCKED_LANGUAGES_OPTIONS doesn't include "typescript" at all, and since the UI only shows these options, users cannot select TypeScript for blocking.

Consider either:

  1. Adding "typescript" (and any other missing canonical names) to this list
  2. Adding a note that common aliases like sh, py, js, ts are automatically covered

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants