Quick start

import { query } from "@anthropic-ai/claude-agent-sdk";
import { createPetriflowGate, safeCodingNet } from "@petriflow/agent-sdk";

const gate = createPetriflowGate([safeCodingNet]);

for await (const message of query({
  prompt: "Fix the bug in auth.py",
  options: {
    allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"],
    hooks: gate.hooks,
  },
})) {
  if ("result" in message) console.log(message.result);
}

The gate.hooks object plugs directly into the Agent SDK's options.hooks. Every tool call passes through the Petri net gate before it executes. Blocked tools get permissionDecision: "deny" and never run.

How it works

Unlike the Claude Code hook (which spawns a new process per event and serializes state to disk), the Agent SDK adapter keeps the GateManager in-process. The manager lives in a closure — state persists naturally across hook callbacks within the same agent session.

// agent SDK hook lifecycle
PreToolUse fires
Callback calls manager.handleToolCall() — returns allow or deny
PostToolUse fires
Callback calls manager.handleToolResult() — deferred transitions fire on success
PostToolUseFailure fires
Callback calls manager.handleToolResult({ isError: true }) — pending cleared, marking unchanged

No state files. No JSON serialization. No restore/save round-trips. The manager's marking mutates in memory, and every hook callback shares the same instance.

Default net

The bundled safeCodingNet gates common Agent SDK tools:

free tools: Read, Glob, Grep, WebSearch gated tools: Write, Edit, Bash, WebFetch, Agent places: idle(1) ready(0) transitions: start(idleready) writeFile(readyready) tools: Write editFile(readyready) tools: Edit runBash(readyready) tools: Bash webFetch(readyready) tools: WebFetch spawnAgent(readyready) tools: Agent

Free tools always pass through. Gated tools need the ready token — available after auto-advance fires the start transition. Override with your own nets for different policies.

// default net in action
Agent calls Glob ("**/*.ts")
OK free tool, always allowed
Agent calls Edit (auth.py)
OK gated, ready token present
Agent calls Bash (npm test)
OK gated, ready token present
Agent calls Read (output.log)
OK free tool

Custom nets

Define your own nets for domain-specific constraints. This example enforces "backup before delete" with a deferred transition — the net only advances after the backup tool succeeds:

import { query } from "@anthropic-ai/claude-agent-sdk";
import { createPetriflowGate, defineSkillNet } from "@petriflow/agent-sdk";

const backupFirst = defineSkillNet({
  name: "backup-before-delete",
  places: ["idle", "ready", "backedUp"],
  terminalPlaces: [],
  freeTools: ["Read", "Glob", "Grep"],
  initialMarking: { idle: 1, ready: 0, backedUp: 0 },
  transitions: [
    { name: "start", type: "auto", inputs: ["idle"], outputs: ["ready"] },
    { name: "backup", type: "auto", inputs: ["ready"], outputs: ["backedUp"],
      tools: ["Backup"], deferred: true },
    { name: "delete", type: "auto", inputs: ["backedUp"], outputs: ["ready"],
      tools: ["Delete"] },
  ],
});

const gate = createPetriflowGate([backupFirst]);

for await (const message of query({
  prompt: "Clean up old deployment artifacts",
  options: {
    allowedTools: ["Read", "Glob", "Grep", "Backup", "Delete"],
    hooks: gate.hooks,
  },
})) {
  if ("result" in message) console.log(message.result);
}
// backup-before-delete in action
Agent calls Delete
BLOCKED no token in backedUp
Agent calls Backup
ALLOWED (deferred — net waits for result)
Backup succeeds → net advances to backedUp
Agent calls Delete
ALLOWED backedUp token present

Composing with rules

Use the rules DSL to generate nets from simple declarations. Each rule compiles to its own net, and all nets compose via AND logic — a tool only fires if every net allows it.

import { createPetriflowGate } from "@petriflow/agent-sdk";
import { compile } from "@petriflow/rules";

const { nets } = compile(`
  require Read before Edit
  require backup before delete
  limit Write to 10 per session
  block rm
`);

const gate = createPetriflowGate(nets);

// gate.hooks is ready to spread into query options

Shadow mode

Set mode: "shadow" to observe decisions without blocking. Tools always execute — use this to evaluate rules against real agent sessions before switching to "enforce".

const gate = createPetriflowGate([safeCodingNet], { mode: "shadow" });

// Tools always execute. Decisions are logged via onDecision callback
// but never block. Use this to evaluate rules before enforcing.

System prompt

Inject constraint descriptions into the agent's system prompt so it knows what's allowed. This reduces wasted tool calls to blocked actions.

const gate = createPetriflowGate([safeCodingNet]);

for await (const message of query({
  prompt: "Refactor the auth module",
  options: {
    allowedTools: ["Read", "Edit", "Glob", "Grep"],
    hooks: gate.hooks,
    systemPrompt: gate.systemPrompt(),
  },
})) {
  console.log(message);
}

gate.systemPrompt() returns a formatted string describing all active nets, their current markings, and which tools are free, gated, or blocked.

Manual approval

Transitions with type: "manual" require confirmation before firing. Pass a confirm callback to handle approval prompts:

const gate = createPetriflowGate([myNet], {
  confirm: async (title, message) => {
    // Your approval logic — prompt user, call API, etc.
    const response = await askUser(`${title}: ${message}`);
    return response === "yes";
  },
});

Without a confirm callback, manual transitions are always denied. This is the key difference from the Claude Code hook, where no UI is available for interactive approval.

Merging hooks

Combine PetriFlow hooks with your own Agent SDK hooks by spreading and concatenating:

const gate = createPetriflowGate([safeCodingNet]);

// Merge PetriFlow hooks with your own
const hooks = {
  ...gate.hooks,
  PostToolUse: [
    ...gate.hooks.PostToolUse,
    { matcher: "Edit|Write", hooks: [myAuditLogger] },
  ],
};

for await (const message of query({
  prompt: "Update the config",
  options: { hooks },
})) {
  console.log(message);
}

Inspecting state

Since the manager lives in-process, you can inspect net state at any time:

const gate = createPetriflowGate([safeCodingNet]);

// After some tool calls...
const status = gate.formatStatus();
console.log(status);
// safe-coding: ready=1

for (const { name, state } of gate.manager.getActiveNets()) {
  console.log(name, state.marking);
}

vs Claude Code hook

Both packages use the same GateManager from @petriflow/gate. The difference is how they integrate:

@petriflow/claude-code @petriflow/agent-sdk integration shell command hooks in-process callbacks state JSON on /tmp per session in-memory (closure) process model new process per event same process approval UI not available (hooks have no UI) confirm callback input transform not possible updatedInput via hooks use case Claude Code CLI sessions programmatic agents

Use @petriflow/claude-code for interactive CLI sessions. Use @petriflow/agent-sdk for agents you build and deploy programmatically.

API reference

createPetriflowGate(nets, opts?)

Creates a gate with in-process hooks for the Claude Agent SDK.

function createPetriflowGate(
  nets: SkillNet<string>[] | ComposeConfig,
  opts?: PetriflowAgentOptions,
): PetriflowAgentGate

Return type

type PetriflowAgentGate = {
  hooks: HooksConfig;
  manager: GateManager;
  systemPrompt: () => string;
  formatStatus: () => string;
}
  • hooks — spread into options.hooks when calling query()
  • manager — the underlying GateManager for state inspection and dynamic net management
  • systemPrompt() — formatted constraint descriptions for the model
  • formatStatus() — current marking across all active nets

Options

type PetriflowAgentOptions = {
  mode?: "enforce" | "shadow";
  confirm?: (title: string, message: string) => Promise<boolean>;
  onDecision?: (decision: GateDecision) => void;
}
  • mode"enforce" (default) blocks disallowed calls. "shadow" logs but never blocks.
  • confirm — callback for manual transition approval. Without it, manual transitions are denied.
  • onDecision — called on every gate decision for logging or telemetry.

safeCodingNet

Bundled default net. Free: Read, Glob, Grep, WebSearch. Gated: Write, Edit, Bash, WebFetch, Agent.

defineSkillNet(config)

Re-exported from @petriflow/gate. Build custom nets. See the gate docs for the full SkillNet type.

createGateManager(nets, opts)

Re-exported from @petriflow/gate. Use directly if you need lower-level control beyond what the hooks provide.