#40880·langchain

An injected tool arg declared with a Pydantic alias is not filtered from `on_tool_start`'s `inputs` and `input_str`

Author: Iris070308Created Sep 28, 2026Updated Sep 28, 2026
Labelsbugcoreexternal

Submission checklist

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-anthropic
  • langchain-classic
  • langchain-core
  • langchain-model-profiles
  • langchain-tests
  • langchain-text-splitters
  • langchain-chroma
  • langchain-deepseek
  • langchain-exa
  • langchain-fireworks
  • langchain-groq
  • langchain-huggingface
  • langchain-mistralai
  • langchain-nomic
  • langchain-ollama
  • langchain-openrouter
  • langchain-perplexity
  • langchain-qdrant
  • langchain-typesafe
  • langchain-xai
  • Other / not sure / general

Related Issues / PRs

No response

Reproduction Steps / Example Code (Python)

python
Repro: an injected tool arg declared with a Pydantic alias is not filtered from
on_tool_start's inputs and input_str.
 
The canonical field name is filtered; the alias is not.
Exit code 1 means the injected value reached the callback.

from typing import Annotated, Any
 
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.tools import InjectedToolArg, StructuredTool
from pydantic import BaseModel, Field
 
SENTINEL = "INTERNAL_SENTINEL"
 
 
class Capture(BaseCallbackHandler):
    def __init__(self):
        self.seen = []
 
    def on_tool_start(self, serialized, input_str, **kwargs: Any):
        self.seen.append((input_str, kwargs.get("inputs")))
 
 
def build(alias):
    class Args(BaseModel):
        x: int
        secret: Annotated[str, InjectedToolArg] = Field(alias=alias)
 
    def action(x: int, secret: Annotated[str, InjectedToolArg]) -> str:
        return "ok"
 
    return StructuredTool.from_function(action, description="Uses an injected secret.", args_schema=Args)
 
 
leaked = []
for alias in ("secret", "private_token"):
    tool = build(alias)
    handler = Capture()
    tool.invoke({"x": 1, alias: SENTINEL}, {"callbacks": [handler]})
    hit = SENTINEL in repr(handler.seen)
    if hit:
        leaked.append(alias)
    props = sorted(tool.tool_call_schema.model_json_schema()["properties"])
    print(f"  alias={alias!r}: model-facing schema {props}, on_tool_start {handler.seen}"
          f"{'   <-- LEAKED' if hit else ''}")
 
print(f"\nFAIL: the injected value reached the callback for {leaked}" if leaked else "\nOK: no injected value reached the callbacks")
raise SystemExit(1 if leaked else 0)

Error Message and Stack Trace (if applicable)

bash
alias='secret': model-facing schema ['x'], on_tool_start [("{'x': 1}", {'x': 1})]
  alias='private_token': model-facing schema ['x'], on_tool_start [("{'x': 1, 'private_token': 'INTERNAL_SENTINEL'}", {'x': 1, 'private_token': 'INTERNAL_SENTINEL'})]   <-- LEAKED
 
FAIL: the injected value reached the callback for ['private_token']

Description

An injected argument declared with a Pydantic alias reaches on_tool_start. BaseTool._filter_injected_args (libs/core/langchain_core/tools/base.py lines 934-968) collects what it should strip from FILTERED_ARGS (line 74), the function's parameter names, and the canonical field names from get_all_basemodel_annotations (lines 957-959). It never adds aliases, and line 968 then filters the input dict by its literal keys, so Field(alias="private_token") never matches anything. The injected value is correctly absent from the model-facing schema -- tool_call_schema only lists x in the reproduction -- so this is not a schema leak. It is just the filter. Both callback channels are affected because run (lines 1057-1082) and arun (lines 1187-1210) filter once and then use the result for inputs and for input_str. If an injected field holds something internal, that value ends up in whatever tracing backend records tool inputs, regardless of whether the call succeeds. The fix probably belongs next to line 959: also collect the field's input names -- alias, validation_alias, and the names inside AliasChoices / AliasPath. _parse_input already deals with aliases via model_fields_set, so there is a pattern to follow. For AliasPath it should delete by path so sibling fields survive.

System Info

langchain-core: 1.6.5 langchain: 1.4.2 langgraph: 1.2.11 pydantic: 2.12.5 Python: 3.11.15 upstream commit tested: 1ef23d6b7f6d83508a8cb531feeb88860e4dec40 OS: Windows 10 + WSL No API key or network access is needed for the reproduction.

Social handles (optional)

No response