# Why does my agent break calls that go through **kwargs?

> Rule AR-11: Avoid **kwargs/...args pass-through layers that hide the real signature; spell the parameters or use ParamSpec/Parameters<>.

A function that takes `(*args, **kwargs)` and hands them on says nothing about what the callee accepts. Add a parameter to the bottom function and the layers above still look correct, and a caller three layers up can pass a misspelled keyword that nothing checks until the bottom call runs. Spell the parameters at the layers people call, or give the wrapper a type that carries the callee's signature.

## Why it happens
**Python.** `**kwargs` collects keyword arguments into a dict, and `*args` collects positionals into a tuple. Inside the function they are plain values, so `inner(*args, **kwargs)` forwards whatever arrived. The wrapper's own signature, which is what `inspect.signature` and a type checker report, is `(*args, **kwargs)`. With the usual annotation `Any`, nothing is checked at the wrapper's call sites.

Python's typing gives three fixes. `Unpack[TypedDict]` types `**kwargs` by key and value (PEP 692, Python 3.12; the docs example is `def foo(**kwargs: Unpack[Movie])`, which expects `name: str` and `year: int`) and has no runtime effect. `ParamSpec` captures a callable's whole parameter list: `P.args` annotates `*args`, `P.kwargs` annotates `**kwargs`, and `Callable[P, T]` reuses the list (PEP 612, Python 3.10). `Concatenate` adds or removes leading parameters. PEP 612 states the problem directly: with `Callable[..., R]`, an ellipsis in place of parameter types means no validation on arguments, so a wrong call type-checks and fails at runtime.

**TypeScript and JavaScript.** `(...args: any[]) => any` is the same hole. TypeScript can carry the signature instead. The TypeScript 3.0 release notes describe generic rest parameters constrained to an array type, which capture the parameter list as a tuple and let you spread it back, as in `bind` returning `(...args: U) => V`. The `Parameters<Type>` utility builds a tuple type from a function type's parameters (for overloads, the last signature), and `ReturnType<Type>` does the same for the result.

## Bad and good
A service layer that adds audit logging around a billing call.

```python tab
# bad: three layers forward everything
def create_invoice(customer_id: str, lines: list[Line], currency: str = "EUR", due_days: int = 30): ...

def billing_call(**kwargs):
    return audit(create_invoice, **kwargs)

def audit(fn, *args, **kwargs):
    log.info(f"calling {fn.__name__}")
    return fn(*args, **kwargs)

billing_call(customer_id="c1", lines=lines, curency="USD")  # typo reaches runtime
```

```ts tab
// bad: the wrapper accepts anything
function createInvoice(customerId: string, lines: Line[], currency = "EUR", dueDays = 30): Invoice { /* ... */ }

function billingCall(...args: any[]) {
  return audit(createInvoice, ...args);
}

function audit(fn: (...a: any[]) => any, ...args: any[]) {
  log.info(`calling ${fn.name}`);
  return fn(...args);
}
```

```python tab
# good: the signature survives the wrapper
from collections.abc import Callable
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def audit(fn: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
    log.info(f"calling {fn.__name__}")
    return fn(*args, **kwargs)

def billing_call(customer_id: str, lines: list[Line], currency: str = "EUR") -> Invoice:
    return audit(create_invoice, customer_id, lines, currency)
```

```ts tab
// good: the signature survives the wrapper
function audit<A extends unknown[], R>(fn: (...args: A) => R, ...args: A): R {
  log.info(`calling ${fn.name}`);
  return fn(...args);
}

function billingCall(...args: Parameters<typeof createInvoice>): Invoice {
  return audit(createInvoice, ...args);
}
```

In the good versions a misspelled keyword or a wrong argument count is reported at the call to `billing_call` or `billingCall`, and renaming a parameter of `createInvoice` shows up as errors at each caller. The `billing_call` wrapper spells its three parameters, which is also the right thing when the wrapper is a public entry point.

## Trade-offs
- **Pass-through is legitimate for thin adapters.** A wrapper whose job is to forward everything, such as a logging layer, a retry layer or a subclass override that adds one argument, gains little from spelling every parameter. Use `ParamSpec` or `Parameters<>` there so it stays generic and checked.
- **Spelling parameters costs edits.** Add an option to the bottom function and each layer that spells it needs the same edit. That is also the point where the agent is told about it.
- **Public entry points should spell parameters.** A caller reading the signature or the generated docs gets the real contract. Reserve pass-through for internal glue.
- **`TypedDict` with `Unpack` suits option bags.** When a function takes many keyword options, a named `TypedDict` documents them once and the checker validates keys. `typing.Unpack` was added in Python 3.11 (https://docs.python.org/3/library/typing.html#typing.Unpack); using `Unpack[TypedDict]` to type `**kwargs` comes from PEP 692, which targets Python 3.12 (https://peps.python.org/pep-0692/). Older versions use `Unpack` from `typing_extensions`, as the typing docs show.
- **`Parameters<>` takes the last overload.** For overloaded functions the utility reports only the last signature, so a wrapper typed that way loses the others.
- **A dataclass or options object** (AR-16) is often the better design than a long forwarded keyword list.

## Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `ANN401` any-type | arguments and returns annotated `Any` (the option `lint.flake8-annotations.allow-star-arg-any` exists; the settings page describes it as "Whether to suppress `ANN401` for dynamically typed `*args` and `**kwargs` arguments", default `false`: https://docs.astral.sh/ruff/settings/) | 2026-10-30 |
| pyright | `reportMissingParameterType`, `reportUnknownParameterType`, `reportUnknownArgumentType` (error in strict) | `*args`/`**kwargs` without annotations, and calls passing unknown-typed values | 2026-10-30 |
| typescript-eslint | `no-explicit-any` (in `recommended`; option `ignoreRestArgs` default `false`) | `(...args: any[])` rest parameters, including `Array<any>` and `readonly any[]` | 2026-10-30 |
| typescript-eslint | `no-unsafe-argument` (in `recommended-type-checked`, needs type information) | spreading an `any[]` into a call | 2026-10-30 |
| tsconfig | `noImplicitAny` (on with `strict`) | parameters with no annotation that would be inferred `any` | 2026-10-30 |

## Rule block for AGENTS.md / CLAUDE.md
```markdown
- Public functions list their parameters. Don't take `**kwargs` / `...args: any[]` and forward them.
- A wrapper that must forward everything types the forwarding:
  Python `Callable[P, R]` with `*args: P.args, **kwargs: P.kwargs`; TypeScript `<A extends unknown[], R>(fn: (...args: A) => R, ...args: A)` or `Parameters<typeof fn>`.
- For keyword option bags use a TypedDict with `Unpack`, or a dataclass.
- Before adding a parameter, list every layer between the entry point and the function, and update each one.
```

## Edge cases
- **Subclass overrides** that take `**kwargs` and call `super().method(**kwargs)` have the same blindness. Spell the new parameter and the ones you use.
- **Decorators** are the most common pass-through layer. The decorator-specific rules, including `functools.wraps`, are in AR-12.
- **`functools.partial` and `bind`** build forwarding layers at runtime. The TypeScript `bind` example in the TypeScript 3.0 notes types the remaining parameters as a tuple.
- **`**kwargs` into a `dict` for storage** (config, metadata) is not forwarding. The dict is data, and AR-16 covers its shape.
- **Positional-only and keyword-only parameters** (the `/` and `*` markers) are part of the real signature. A forwarding layer that flattens them changes which calls are valid.

## What LLMGRAPH shows
A function passed to `audit(fn, *args, **kwargs)` prints `no calls to it found in this repo` and `passed as a value in 1 place: calls through it are not visible`.

## References
- Python typing: `ParamSpec`, `Concatenate`, `Unpack` with `TypedDict`: https://docs.python.org/3/library/typing.html
- PEP 612 (Parameter Specification Variables, final, Python 3.10): https://peps.python.org/pep-0612/
- PEP 692 (`Unpack` for `**kwargs` typing, final, Python 3.12): https://peps.python.org/pep-0692/
- Ruff ANN401: https://docs.astral.sh/ruff/rules/any-type/
- Pyright configuration: https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md
- TypeScript 3.0 release notes (generic rest parameters): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html
- TypeScript utility types (`Parameters`, `ReturnType`): https://www.typescriptlang.org/docs/handbook/utility-types.html
- TSConfig reference: https://www.typescriptlang.org/tsconfig/
- typescript-eslint: https://typescript-eslint.io/rules/no-explicit-any/ · https://typescript-eslint.io/rules/no-unsafe-argument/

## Changelog
- 2026-10-30: draft 1.
- 2026-10-30: verify items resolved (docs check)

## Related

- [AR-12: Do decorators hide function signatures from coding agents?](https://beta.llmgraph.in/agent-readable/ar-12.md)
- [AR-13: Why is Any a blind spot for coding agents?](https://beta.llmgraph.in/agent-readable/ar-13.md)
- [AR-16: Why do dict-shaped objects make agents guess?](https://beta.llmgraph.in/agent-readable/ar-16.md)
- [AR-25: What to check before changing a function signature?](https://beta.llmgraph.in/agent-readable/ar-25.md)

Last verified 2026-10-30
