Turnstile
September 19, 2026 ยท View on GitHub
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.
| Host | Enforcement point | Review behavior |
|---|---|---|
| Claude Code | Pre-tool hook | Native approval |
| OpenCode | Before-tool plugin | Block |
| Pi | Tool-call extension | Exact-action confirmation when UI exists; otherwise block |
| Gemini CLI | Before-tool hook | Block |
| Cursor | Generic pre-tool hook | Block |
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.
| Tool | First-release behavior in enforce mode |
|---|---|
Read, Write, Edit | Check workspace and sensitive paths, then evaluate task alignment with Jev |
Bash | Request native approval; no automatic shell authorization |
Other tools, including MCP, Glob, Grep, and WebFetch | Request 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.
| Condition | Decision |
|---|---|
| Explicit denial or failed argument constraint | Deny |
| Explicit review or unknown tool | Review by default |
Any semantic score at least 0.85 | Deny |
Any semantic score at least 0.35, below 0.85 | Review |
All scores below 0.35 | Allow |
| Missing goal, disabled Jev, timeout, rejected request, invalid response | Review |
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
WriteandEditarguments are included. Redaction is not a complete secret or PII detector. Readfile 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
- All agent integrations
- Claude Code installation and operation
- Policy and SDK reference
- Architecture and failure behavior
- Threat model and coverage
- Validation and live checks
- Roadmap and changelog
- Contributing, agent instructions, and security reporting
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.