An injected tool arg declared with a Pydantic alias is not filtered from `on_tool_start`'s `inputs` and `input_str`
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)
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)
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
Source: langchain-ai/langchain