mcpbeat Sign in

Pysa JSON Models Agent Skill

Use when reading, writing, or debugging Pysa JSON model output (.models files). Use when working with taint models that describe sources, sinks, TITO, sanitizers, or issues in JSON format.

3k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
7170
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/facebook/pyre-check --skill pysa-json-models

The instruction itself

29 sections, as written by the author

Pysa JSON Model Syntax

Overview

Pysa outputs analysis results as newline-delimited JSON (NDJSON). Each line is a self-contained JSON object with "kind" and "data" fields. There are two kinds: "model" (a callable's taint behavior) and "issue" (a detected vulnerability).

Top-Level Wrapper

Every JSON object is wrapped as:

{"kind": "model", "data": { ... }}
{"kind": "issue", "data": { ... }}

Models and issues are separate top-level objects. Issues are never nested inside models.

Model Structure

{
  "callable": "module.function_name",
  "filename": "module.py",
  "callable_line": 42,
  "sources": [ ... ],
  "sinks": [ ... ],
  "tito": [ ... ],
  "parameter_sources": [ ... ],
  "global_sanitizer": { ... },
  "parameters_sanitizer": { ... },
  "sanitizers": [ ... ],
  "modes": [ "Obscure", ... ]
}

All fields except callable are optional. Empty fields are omitted entirely — never include empty arrays [] or objects {}.

Note: "sources" represents *generations* (taint produced by the function). This is distinct from "parameter_sources" which represents sources on parameters.

Callables

  • Regular functions: "module.function_name"
  • Methods: "module.ClassName.method_name"
  • Attribute models: "Obj{module.ClassName.attribute_name}"

Modes

Available modes: Obscure, SkipObscure, SkipAnalysis, SkipOverrides, AnalyzeAllOverrides, Entrypoint, IgnoreDecorator, SkipModelBroadening, InferSelfTito, InferArgumentTito.

Port Entries (sources/sinks/tito/parameter_sources)

Each of sources, sinks, tito, and parameter_sources is a list of port entries:

{
  "port": "<port>",
  "taint": [ <trace_element>, ... ]
}

Port Formats

| Port | Meaning |

|------|---------|

| result | Return value |

| result[field] | Return value with access path |

| formal(name, position=N) | Named parameter at position N |

| formal(name, position=N, positional_only) | Positional-only parameter |

| formal(name, position=N)[field][subfield] | Parameter with access path |

| formal(*args, position=N) | Variadic positional args |

| formal(**kwargs) | Variadic keyword args |

| formal(**kwargs, excluded=[x]) | Kwargs excluding named params |

| formal($global, position=0) | Global/attribute model port |

Key rule: position is always included in formal() ports (e.g., formal(x, position=0), not formal(x)).

For sources/parameter_sources, the port indicates where taint is *produced* (typically result or result[field]).

For sinks, the port indicates where taint is *consumed* (typically formal(...) with optional access path).

For tito, the port indicates the *input* side (always formal(...)). The output side is encoded inside the taint entry via return_paths.

Trace Elements (Call Info)

Each element in a "taint" list is a trace element keyed by its call info. There are four call info variants:

1. Declaration — user-specified taint from .pysa model files

{
  "kinds": [ { "kind": "Test" } ],
  "declaration": null
}

This is the leaf of the trace — the taint is directly declared by the user, not propagated from another function.

2. Origin — taint at a direct call site (first hop)

{
  "kinds": [ { "kind": "Test", "length": 0 } ],
  "origin": { "line": 16, "start": 15, "end": 20 }
}

Represents a call to a function with a user-declared model. The origin location is where the call happens. "call_site" may also be present (e.g., "call_site": "16:4-16:21").

3. Call — taint propagated through a call chain

{
  "kinds": [ { "kind": "Test", "length": 2 } ],
  "call": {
    "position": { "line": 20, "start": 8, "end": 22 },
    "resolves_to": [ "module.callee_function" ],
    "port": "result[a]"
  }
}

Represents a hop: taint flows from callee_function at the given port. resolves_to is a list (can have multiple callees for overrides). "call_site" may also appear inside the "call" object.

4. Tito — taint-in-taint-out propagation

{
  "kinds": [ { "return_paths": { "": 0 }, "kind": "LocalReturn" } ],
  "tito": {}
}

Used in "tito" port entries. The "tito" call info is always {} (empty object, never null).

Kind Entries

Each "kinds" list entry describes a taint kind with optional metadata:

{
  "kind": "Test",
  "length": 2,
  "leaves": [ { "name": "pysa._test_source", "port": "leaf:return" } ],
  "features": [ { "always-via": "special_source" } ],
  "local_features": [ { "always-via": "special_sink" } ],
  "return_paths": { "": 0 }
}

| Field | Description |

|-------|-------------|

| kind | Source/sink kind name (e.g., Test, UserControlled, SQL, RemoteCodeExecution). For TITO: LocalReturn |

| length | Trace length (hops to closest leaf). Omitted when 0 |

| leaves | Set of leaf declarations this taint originates from. Each leaf: {"name": "qualified.name", "port": "leaf:return"} |

| features | Propagated breadcrumbs from callees |

| local_features | Breadcrumbs introduced in this function |

| return_paths | (TITO only) Maps access paths to collapse depths, e.g. {"": 0} (identity), {"[field]": 3} (into field) |

Leaf Ports

  • leaf:return — source on return value
  • leaf:arg — sink on argument
  • leaf:arg[field] — sink on argument field
  • producer:N:formal(M) — cross-repository producer
  • anchor:formal(M) — cross-repository anchor

Trace-Level Local Features and Tito Positions

Local features and tito positions can appear at the trace element level (outside kinds):

{
  "kinds": [ ... ],
  "local_features": [ { "has": "first-index" }, { "first-index": "<numeric>" } ],
  "tito_positions": [ { "line": 71, "start": 34, "end": 37 } ],
  "origin": { ... }
}

local_features (trace-level): Breadcrumbs introduced at this particular call site within the current function. These describe what happens to the data at this point in the flow — for example, first-index records the first dictionary key or list index used to access tainted data. Unlike kind-level local_features (which are specific to a taint kind), trace-level local features apply across all kinds in the trace element.

tito_positions: Locations in the current function where taint was propagated via taint-in-taint-out. Each entry is a source position (line, start, end) pointing to an expression where tainted data flowed through a TITO function call. This helps trace how data moved through the function before reaching a sink or being returned.

Features (Breadcrumbs)

Features use a prefix to indicate whether they appear on all paths (always-) or some paths (no prefix):

{ "always-via": "special_source" }
{ "via": "tito" }
{ "always-type": "bool" }
{ "has": "first-index" }
{ "first-index": "<numeric>" }
{ "first-index": "arg" }
{ "has": "first-field" }
{ "first-field": "attribute_name" }

Sanitizers

Global Sanitizer (applies to entire function)

"global_sanitizer": {
  "sources": "All",
  "sinks": [ "SQL" ],
  "tito": { "sources": [ "UserControlled" ], "sinks": "All" }
}

Each of sources, sinks, tito is optional. Values are either "All" or a list of kind names. The tito sub-object has its own sources/sinks.

Parameters Sanitizer (applies to all parameters)

Same format as global sanitizer, under key "parameters_sanitizer".

Per-Root Sanitizers

"sanitizers": [
  {
    "port": "formal(x, position=0)",
    "sources": "All",
    "sinks": [ "TestSink" ]
  }
]

Issue Structure

Issues are separate top-level objects, not nested inside models:

{
  "kind": "issue",
  "data": {
    "callable": "module.function_name",
    "callable_line": 19,
    "code": 5002,
    "line": 21,
    "start": 15,
    "end": 16,
    "filename": "module.py",
    "message": "Data from [Test] source(s) may reach [Test] sink(s)",
    "traces": [
      {
        "name": "forward",
        "roots": [ <trace_elements> ]
      },
      {
        "name": "backward",
        "roots": [ <trace_elements> ]
      }
    ],
    "features": [ { "always-via": "special_source" } ],
    "sink_handle": {
      "kind": "Call",
      "callee": "module.sink_function",
      "index": 0,
      "parameter": "formal(arg)"
    },
    "master_handle": "module.function:5002:0:Call|module.sink|0|formal(arg):md5hash"
  }
}

Trace elements in roots use the same format as model trace elements (kinds/call/origin/declaration).

Sink Handle Variants

| Variant | Fields |

|---------|--------|

| Call | callee, index, parameter |

| Global | callee, index |

| Return | (none) |

| LiteralStringSink | sink |

| ConditionalTestSink | sink |

| StringFormat | callee, index, parameter_index |

parameter in sink handle uses the callee's parameter name (e.g., formal(arg) for _test_sink(arg)), not the caller's argument variable name.

Common Mistakes

| Mistake | Correct |

|---------|---------|

| "formal(x)" | "formal(x, position=0)" — always include position |

| "declaration": {} or "decl": null | "declaration": null — exact key and value matter |

| "tito": null (call info) | "tito": {} — empty object, not null |

| TITO port = "result" | TITO port = "formal(...)" — port is the *input* side |

| Issues inside model "errors" | Issues are separate {"kind": "issue"} objects |

| Including "sources": [] | Omit empty fields entirely |

| Leaves: {"kind": "Test", "name": "..."} | Leaves: {"name": "pysa._test_source", "port": "leaf:return"} |

| sink_handle parameter = caller's variable | sink_handle parameter = callee's parameter name |

Complete Example

Given:

# example.py
from pysa import _test_source, _test_sink

def get_source():
    return _test_source()

def flow():
    x = get_source()
    _test_sink(x)

Model for _test_source (user-declared source):

{
  "kind": "model",
  "data": {
    "callable": "pysa._test_source",
    "filename": "pysa/__init__.pyi",
    "callable_line": 5,
    "sources": [
      {
        "port": "result",
        "taint": [
          {
            "kinds": [
              {
                "features": [ { "always-via": "special_source" } ],
                "kind": "Test"
              }
            ],
            "declaration": null
          }
        ]
      }
    ],
    "modes": [ "Obscure" ]
  }
}

Model for get_source (inferred source via call):

{
  "kind": "model",
  "data": {
    "callable": "example.get_source",
    "filename": "example.py",
    "callable_line": 4,
    "sources": [
      {
        "port": "result",
        "taint": [
          {
            "kinds": [
              {
                "local_features": [ { "always-via": "special_source" } ],
                "leaves": [ { "name": "pysa._test_source", "port": "leaf:return" } ],
                "kind": "Test"
              }
            ],
            "origin": { "line": 5, "start": 11, "end": 25 }
          }
        ]
      }
    ]
  }
}

Issue for flow:

{
  "kind": "issue",
  "data": {
    "callable": "example.flow",
    "callable_line": 7,
    "code": 5002,
    "line": 9,
    "start": 15,
    "end": 16,
    "filename": "example.py",
    "message": "Data from [Test] source(s) may reach [Test] sink(s)",
    "traces": [
      {
        "name": "forward",
        "roots": [
          {
            "kinds": [
              {
                "features": [ { "always-via": "special_source" } ],
                "leaves": [ { "name": "pysa._test_source", "port": "leaf:return" } ],
                "length": 1,
                "kind": "Test"
              }
            ],
            "call": {
              "position": { "line": 8, "start": 8, "end": 20 },
              "resolves_to": [ "example.get_source" ],
              "port": "result"
            }
          }
        ]
      },
      {
        "name": "backward",
        "roots": [
          {
            "kinds": [
              {
                "local_features": [ { "always-via": "special_sink" } ],
                "leaves": [ { "name": "pysa._test_sink", "port": "leaf:arg" } ],
                "kind": "Test"
              }
            ],
            "origin": { "line": 9, "start": 15, "end": 16 }
          }
        ]
      }
    ],
    "features": [
      { "always-via": "special_source" },
      { "always-via": "special_sink" }
    ],
    "sink_handle": {
      "kind": "Call",
      "callee": "pysa._test_sink",
      "index": 0,
      "parameter": "formal(arg)"
    },
    "master_handle": "example.flow:5002:0:Call|pysa._test_sink|0|formal(arg):abcdef1234567890"
  }
}

Reference

  • Model types defined in: source/interprocedural_analyses/taint/model.ml
  • Call info, frames, taint serialization: source/interprocedural_analyses/taint/domains.ml
  • Feature/breadcrumb format: source/interprocedural_analyses/taint/features.ml
  • Issue format: source/interprocedural_analyses/taint/issue.ml
  • Sanitizer format: source/interprocedural_analyses/taint/sanitize.ml

Other skills for the same job

different authors, same section of the catalogue
Doc Coauthoring
by anthropics
vendor ×10

Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.

4k tokens
Changelog Generator
by frostant
×9

Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.

774 tokens
Test Driven Development
by w95
×7

Use when implementing any feature or bugfix, before writing implementation code

2k tokens
Writing Plans
by ZhanlinCui
×4

Use when you have a spec or requirements for a multi-step task, before touching code

816 tokens
Writing Skills
by ZhanlinCui
×4

Use when creating new skills, editing existing skills, or verifying skills work before deployment

26k tokens scripts
Crafting Effective Readmes
by softaworks
×3

Use when writing or improving README files. Not all READMEs are the same — provides templates and guidance matched to your audience and project type.

15k tokens
Humanizer
by softaworks
×3

| Remove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's inflated symbolism, promotional language, superficial -ing analyses, vague attributions, em dash overuse, rule of three, AI vocabulary words, negative parallelisms, and excessive conjunctive phrases.

6k tokens
Opentrons Integration
by christophacham
×3

Official Opentrons Protocol API for OT-2 and Flex robots. Use when writing protocols specifically for Opentrons hardware with full access to Protocol API v2 features. Best for production Opentrons protocols, official API compatibility. For multi-vendor automation or broader equipment control use pylabrobot.

9k tokens scripts

How to use it

Copy the folder

Take facebook/pysa-json-models from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.