Skip to main content

toolcall-linter

toolcall-linter

Local, offline, read-only linter that cross-checks agent transcript tool calls against declared schemas.

InstallQuick StartMethodologyFormatsCI

Python 3.10+ MIT License Fully offline No telemetry


What problem does this solve?

AI agents call tools hundreds of times per session. When an agent passes a wrong argument type, forgets a required parameter, or hallucinates a tool name, the failure often surfaces far downstream as a confusing runtime error. Debugging those failures means manually tracing through logs and comparing tool calls against schemas.

toolcall-linter makes those mistakes visible immediately, at lint time, with a single command.

toolcall-linter transcript.jsonl --tools tools.json

It reads the agent transcript, loads the declared tool schemas, validates every call against JSON Schema, and reports exactly where the contract is broken.


How it works

toolcall-linter workflow

  1. Parse the transcript — accepts Claude Code JSONL or OpenAI messages arrays.
  2. Load the schema sourcetools.json, an MCP server over stdio (mcp-stdio:<cmd>), or HTTP (mcp-http:<url>).
  3. Match and validate — every tool call is looked up by name and validated with jsonschema.
  4. Report violations — line-precise errors in text or JSON, with a nonzero exit code when anything fails.

What it catches

Violation types

Failure class Example Why it matters
Undefined tool {"name": "frobnicate"} Agent invented a tool that does not exist in the schema.
Missing required argument read_file without file_path Runtime call will fail or read the wrong thing.
Wrong type file_path: 123 String path expected, integer received.
Extra argument read_file with encoding: "utf-8" Schema forbids additional properties; agent is drifting.
Invalid enum value output_mode: "full_text" Agent picked a mode the tool does not support.

Install

# Recommended: install with pipx (isolated, global CLI)
pipx install .
pipx install git+https://github.com/Victorchatter/toolcall-linter

# Or in a local venv
python -m venv .venv
source .venv/bin/activate  # .venv\Scripts\activate on Windows
pip install -e .

No network calls, no telemetry, no credentials required.


Quick start

Create a tools.json with the schemas your agent is supposed to respect:

{
  "tools": [
    {
      "name": "read_file",
      "inputSchema": {
        "type": "object",
        "properties": {
          "file_path": { "type": "string" },
          "limit": { "type": "integer", "minimum": 1 }
        },
        "required": ["file_path"],
        "additionalProperties": false
      }
    },
    {
      "name": "Bash",
      "inputSchema": {
        "type": "object",
        "properties": {
          "command": { "type": "string", "minLength": 1 },
          "description": { "type": "string" }
        },
        "required": ["command", "description"],
        "additionalProperties": false
      }
    }
  ]
}

Create a transcript.jsonl that records what the agent actually did:

{"type": "tool_use", "name": "read_file", "input": {"file_path": "/etc/passwd"}}
{"type": "tool_use", "name": "read_file", "input": {"limit": 10}}
{"type": "tool_use", "name": "Bash", "input": {"command": "git status"}}
{"type": "tool_use", "name": "frobnicate", "input": {"x": 1}}

Run the linter:

toolcall-linter transcript.jsonl --tools tools.json

Output:

transcript.jsonl:2 read_file: 'file_path' is a required property
transcript.jsonl:3 Bash: 'description' is a required property
transcript.jsonl:4 frobnicate: tool 'frobnicate' is not declared in schema source

Exit code is 1 because violations were found.

Infer a schema from a tape

If you have an agent transcript but no declared tools.json, infer one from the tape:

toolcall-linter infer transcript.jsonl -o tools.json --pretty

The infer subcommand inspects every tool call, unions the argument keys seen for each tool, marks keys that appear in every call as required, infers JSON Schema types, and emits string enums when a property has ten or fewer distinct values. It writes an MCP-style tools.json and, by default, validates that schema against the source tape:

OK: inferred schema validates against 1 transcript(s)

Use --no-validate to skip the validation pass. Once you have tools.json, lint other transcripts with the usual command:

toolcall-linter another-transcript.jsonl --tools tools.json

You can pass multiple transcripts to infer to build a schema from a larger corpus:

toolcall-linter infer transcripts/*.jsonl -o tools.json --pretty

JSON output

toolcall-linter transcript.jsonl --tools tools.json --format json
{
  "ok": false,
  "violation_count": 1,
  "violations": [
    {
      "file": "transcript.jsonl",
      "line": 2,
      "tool": "read_file",
      "severity": "error",
      "message": "'file_path' is a required property",
      "schema_path": ""
    }
  ]
}

SARIF output

toolcall-linter transcript.jsonl --tools tools.json --format sarif > results.sarif

SARIF output includes runs[0].results with ruleId, message.text, and locations[0].physicalLocation mapped to the transcript file and line number. Use it with any SARIF-compatible CI viewer or the GitHub Actions workflow below.


Methodology

Fail fast at lint time

The guiding principle is to move the detection of contract violations as early as possible — from runtime debugging to a fast, deterministic lint step. Every agent transcript is a record of tool calls that should already satisfy the tool schemas. Treating it as a lintable artifact lets you:

  • catch schema drift after prompt changes
  • compare an agent's actual calls against its intended capability set
  • gate CI on transcript correctness
  • reproduce and bisect failures with exact line numbers

Schema-first validation

toolcall-linter does not guess what a tool should accept. It uses the actual declared JSON Schema (MCP inputSchema, OpenAI function parameters, or a tools.json file). This keeps the linter honest: if the schema is wrong, the linter is wrong in the same way, which forces the schema itself to become the source of truth.

Read-only and local

The tool never modifies the transcript, never sends data anywhere, and never calls the tools it inspects. This makes it safe to run on production transcripts containing file paths, queries, or other sensitive arguments.


Supported schema sources

Static tools.json

toolcall-linter transcript.jsonl --tools tools.json

Accepts either a JSON array of tools or an object with a "tools" key. Each tool must provide name and inputSchema (MCP style) or parameters (OpenAI style).

MCP server over stdio

toolcall-linter transcript.jsonl --tools "mcp-stdio:python -m my_mcp_server"

The linter spawns the command, sends a JSON-RPC tools/list request, and uses the returned schemas.

MCP server over HTTP

toolcall-linter transcript.jsonl --tools mcp-http:http://localhost:3000

Queries http://localhost:3000/tools/list and extracts the tool list from the response.


Supported transcript formats

Claude Code JSONL

Each line is a JSON object. The linter extracts tool calls from:

  • { "name": "...", "input": {...} }
  • { "tool_calls": [...] } with function.name and function.arguments
  • legacy function_call objects

OpenAI messages array

A JSON file starting with [ is treated as an OpenAI messages array. The linter reads assistant messages containing tool_calls:

{
  "role": "assistant",
  "tool_calls": [
    {
      "id": "call_123",
      "type": "function",
      "function": {
        "name": "read_file",
        "arguments": "{\"file_path\":\"/etc/passwd\"}"
      }
    }
  ]
}

The arguments string is parsed as JSON before validation.


CLI reference

toolcall-linter <transcript>... --tools <source> [--format text|json]
toolcall-linter infer <transcript>... -o <tools.json> [--pretty] [--no-validate]

Lint

Argument Description
<transcript>... One or more paths to transcript files (JSONL or JSON array). Supports globs.
--tools <source> Schema source: tools.json, mcp-stdio:<cmd>, or mcp-http:<url>. Required.
`--format text json

Infer

Argument Description
<transcript>... One or more paths to transcript files to inspect. Supports globs.
-o, --output <tools.json> Output file for the inferred MCP-style tool schema. Required.
--pretty Write formatted JSON with indentation.
--no-validate Skip validating the inferred schema against the source transcript(s).

Exit codes

Code Meaning
0 No violations found.
1 One or more violations found.
2 CLI or configuration error (bad file path, malformed schema, etc.).

Terminal demo

Terminal demo


CI integration

Use the nonzero exit code to block bad transcripts in CI:

# .github/workflows/lint-transcripts.yml
name: lint-transcripts
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv tool install .
      - run: toolcall-linter transcripts/*.jsonl --tools tools.json

GitHub Actions with SARIF

Generate a SARIF file and upload it to GitHub Advanced Security so violations appear inline on the PR diff:

# .github/workflows/lint-transcripts-sarif.yml
name: lint-transcripts-sarif
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv tool install .
      - run: toolcall-linter transcripts/*.jsonl --tools tools.json --format sarif > toolcall-linter.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: toolcall-linter.sarif

Because the linter is read-only and offline, it is safe to run on every commit.

GitHub Action

The repo provides a reusable composite action at action.yml. It installs the linter from the checked-out repo, runs it on the supplied transcript(s), writes a report, and exposes the finding count as an output.

# .github/workflows/lint-toolcalls.yml
name: lint-toolcalls
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Victorchatter/toolcall-linter@v0.4.0
        with:
          transcript: transcripts/*.jsonl
          tools: tools.json
          format: sarif
          fail-on-blockers: true

Inputs:

Input Required Default Description
transcript yes Path to transcript(s). Glob patterns are supported.
tools yes Path to tools.json or MCP source.
format no sarif text, json, or sarif.
fail-on-blockers no true Fail the step when findings are reported.

Outputs:

Output Description
findings-count Number of findings reported.
report-path Path to the generated report file.

pre-commit hook

Add the hook to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/Victorchatter/toolcall-linter
    rev: v0.4.0
    hooks:
      - id: toolcall-linter
        args: [--tools, tools.json, --format, sarif]

The default hook runs on JSON files. Override files or types to lint committed transcripts. Pass --warn-only to report issues without blocking the commit.


Development

# Install in editable mode
python -m pip install -e .

# Run the synthetic end-to-end test
python selfcheck.py

Expected output:

Found 5 violations
 - 'file_path' is a required property
 - 123 is not of type 'string'
 - Additional properties are not allowed ('encoding' was unexpected)
 - tool 'frobnicate' is not declared in schema source
 - 'maybe' is not one of ['yes', 'no']
PASS

Design

The v1 design spec is available at:


License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toolcall_linter-0.4.0.tar.gz (24.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toolcall_linter-0.4.0-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

File details

Details for the file toolcall_linter-0.4.0.tar.gz.

File metadata

  • Download URL: toolcall_linter-0.4.0.tar.gz
  • Upload date:
  • Size: 24.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for toolcall_linter-0.4.0.tar.gz
Algorithm Hash digest
SHA256 c9031c825776dcc62d1af56b74f36382d967f994f983bf07ddabb23edeccbb2e
MD5 7d7720db9a654c9dd43fa1a0f02445aa
BLAKE2b-256 64d877234a3770903b510eded1698d101d1bbef70c6de8880f74a16a573ba02a

See more details on using hashes here.

File details

Details for the file toolcall_linter-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for toolcall_linter-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9998c12b05a25c3d45db48f025759bea84b154b825689fa3ce3edd09a91f5d59
MD5 2eafe4e3081fed0152daa3a6dfea6302
BLAKE2b-256 03319b539f7cd535713e25607f5636b91a69a8fd11376cc015d096651927f4e2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page