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.
Callback calls
manager.handleToolCall() — returns allow or denyCallback calls
manager.handleToolResult() — deferred transitions fire on successCallback calls
manager.handleToolResult({ isError: true }) — pending cleared, marking unchangedNo 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 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.
OK free tool, always allowed
OK gated, ready token present
OK gated, ready token present
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);
} BLOCKED no token in backedUp
ALLOWED (deferred — net waits for result)
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:
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.hookswhen callingquery() - manager — the underlying
GateManagerfor 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.