Turnstile

September 19, 2026 ยท View on GitHub

CI License: Apache-2.0

Check an AI agent's next action before it executes.

Turnstile combines deterministic permissions with Jev checks for task drift, unauthorized disclosure, and instructions from untrusted content. It returns allow, review, or deny, and records the decision for inspection and threshold replay.

Turnstile integrates with Claude Code, OpenCode, Pi, Gemini CLI, and Cursor on laptops and servers. Applications can use the same TypeScript engine to guard their own tool executors.

Status: experimental alpha. The repository runs from source with Bun. There is no published npm package. The adapters check selected tool boundaries; it is not an endpoint sandbox or a replacement for operating-system permissions. Start in observe mode and read the coverage and limits before enabling enforcement.

Try it

Requires Bun 1.3.13 or later. Development and CI use 1.3.13.

git clone https://github.com/zyphr-labs/turnstile.git
cd turnstile
bun install --frozen-lockfile
bun run demo

The offline example permits a local note and blocks publication before the executor is called:

Local note: allow; executor called.
Public upload: deny; executor not called.

This example uses fixed judgments so it needs no API key. It demonstrates enforcement, not model accuracy. Run bun run verify for formatting, type checks, tests, and the example.

Guard an application tool

The example below runs inside this source checkout. The caller supplies trusted intent and policy; an agent must not choose its own permissions.

import { createGuard, createJevJudge } from "./src/index";

const guard = createGuard({
  policy: {
    version: 1,
    mode: "enforce",
    tools: {
      "note.write": {
        effect: "allow",
        argumentEquals: { path: "notes/summary.txt" },
      },
      "note.publish": { effect: "deny" },
    },
  },
  // Explicitly opts in to sending context to TypeSafe.
  judge: createJevJudge({ apiKey: process.env.TYPESAFE_API_KEY! }),
});

await guard.execute(
  {
    userGoal: "Save a local summary in notes/summary.txt",
    tool: "note.write",
    arguments: { path: "notes/summary.txt", content: "Meeting summary." },
  },
  async (args) => Bun.write(String(args.path), String(args.content)),
);

Create the notes directory before running that example. execute passes a snapshot of the checked arguments to the callback. In enforce mode it throws ActionBlocked for review or denial and never invokes the callback. check returns a decision without executing anything. The application must honor that decision.

Connect an agent

See the integration guide for setup and removal. OpenCode uses a project plugin; Pi uses an extension loaded with -e; Gemini CLI and Cursor use project hook settings. All share one policy format.

HostEnforcement pointReview behavior
Claude CodePre-tool hookNative approval
OpenCodeBefore-tool pluginBlock
PiTool-call extensionExact-action confirmation when UI exists; otherwise block
Gemini CLIBefore-tool hookBlock
CursorGeneric pre-tool hookBlock

OpenCode and Pi have real tool-execution checks using a loopback fixture model. Gemini, Cursor, and Claude have protocol tests; those are not claims of full host validation.

Claude Code example

Initialize configuration in a project you own:

bun run cli init --project /absolute/path/to/project
bun run cli claude-settings \
  --config /absolute/path/to/project/.turnstile/config.json \
  > /absolute/path/to/project/turnstile-hooks.json

Add .turnstile/ and turnstile-hooks.json to that project's .gitignore. From the project directory, launch:

claude --settings ./turnstile-hooks.json

Initialization leaves existing hooks and user settings alone. Defaults are observe mode and Jev disabled. To evaluate actions with Jev, set jev.enabled to true in .turnstile/config.json and provide TYPESAFE_API_KEY in Claude's environment. To block or request approval, also set policy.mode to "enforce".

Review the installation guide for data handling, approval behavior, removal, and host requirements. A policy allow returns no hook override, so Claude's own permissions still apply.

ToolFirst-release behavior in enforce mode
Read, Write, EditCheck workspace and sensitive paths, then evaluate task alignment with Jev
BashRequest native approval; no automatic shell authorization
Other tools, including MCP, Glob, Grep, and WebFetchRequest native approval by default

How a decision works

trusted goal + proposed action + optional untrusted evidence
                         |
                 deterministic policy
                 /        |          \
              deny      review       allow
               |          |            |
               |          |      Jev judgments
               |          |            |
               +----------+------------+
                          |
                   decision receipt
                          |
              observe or enforce at executor

Jev never grants authority that policy denied. Explicit review rules also remain review rules. Model checks run only after deterministic permission succeeds.

ConditionDecision
Explicit denial or failed argument constraintDeny
Explicit review or unknown toolReview by default
Any semantic score at least 0.85Deny
Any semantic score at least 0.35, below 0.85Review
All scores below 0.35Allow
Missing goal, disabled Jev, timeout, rejected request, invalid responseReview

The thresholds are starting values, not calibrated guarantees. Jev scores are model estimates. They do not prove that an action is authorized, safe, or influenced by an injection.

Observe mode records the same policy verdict but does not enforce it. Configuration, input, and audit failures still stop the SDK executor or produce a blocking hook error. See failure behavior.

Inspect and replay decisions

The endpoint adapters append JSON lines to .turnstile/decisions.jsonl. Receipts contain verdicts, reason codes, model scores, latency, and fingerprints, without raw prompts or tool arguments.

bun run cli replay /absolute/path/to/project/.turnstile/decisions.jsonl \
  --review 0.45 --deny 0.90

Replay shows which verdicts would change using the recorded scores. It makes no model calls and preserves hard denials and unavailable-model reviews. It does not re-evaluate content or simulate a new tool policy.

Data and trust

  • Jev is a hosted API. Enabling it sends the current goal, tool name, arguments, and supplied evidence to TypeSafe after limited credential redaction. File contents in Write and Edit arguments are included. Redaction is not a complete secret or PII detector.
  • Read file contents and full Claude transcripts are not collected. The adapters cannot infer the contents of files they have not seen. Their untrusted-evidence lists are empty; application integrations can supply evidence explicitly.
  • Claude, Gemini, and Cursor session files store the latest raw user prompt locally with mode 0600. Session-end hooks attempt cleanup. Crashed sessions may leave files behind; there is no automatic retention cleanup. OpenCode and Pi keep prompt state in process memory.
  • Audit fingerprints are unsalted hashes, not anonymization. Audit files remain local, with no telemetry or upload service.
  • Same-user code can change hooks, configuration, or files. Path checks are not atomic filesystem mediation. Use OS isolation when that is part of your threat model.

Project documentation

Turnstile is a separate project from Zyphr and Lantern. It is maintained in zyphr-labs, Hari's security lab. Contributions are licensed under Apache-2.0.