pi-jev-auto-mode
Jev (TypeSafe System One) backed auto mode for the Pi coding agent: semantically auto-approves bash, write, and edit tool calls and fails closed when a decision cannot be made.
Package details
Install pi-jev-auto-mode from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-jev-auto-mode- Package
pi-jev-auto-mode- Version
0.6.0- Published
- Oct 18, 2026
- Downloads
- 1,807/mo · 75/wk
- Author
- jomatsu
- License
- MIT
- Types
- extension
- Size
- 225 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-jev-auto-mode
Auto mode for the Pi coding agent backed by
decision models, with Jev (TypeSafe System One) as the default. OpenAI Decisions,
Perplexity Decisions, Cloudflare Clef, and a local Strands Decider server are also supported; see
docs/providers.md. Pi has no built-in permission system,
so a gate either exists as an extension or it does not exist at all. This one judges
bash, write, and edit tool calls semantically and fails closed whenever a decision
cannot be made.
Status: milestones 1–3 are complete. The deterministic envelope, the Jev engine, real-API calibration, settings, policy notes, per-rule threshold tuning, and decision records are implemented and tested without network calls. See
docs/design.mdfor the roadmap anddocs/calibration.mdfor the measured probabilities behind every threshold.
What it does
The gate has two layers, in this order:
- Deterministic policy — hard-deny commands, your allow/deny patterns, dangerous-command detection, protected paths. Hard-deny is never handed to the semantic layer.
- Semantic judgment (Jev) — only the calls the first layer escalated.
hard-deny → block (never reaches Jev)
your deny pattern → block
your allow pattern → allow (recorded)
your safeCommands → run, no record
dangerous pattern match → Jev (even when the command looks read-only)
read-only builtin → run, no record
in-project write/edit → run, no record
everything else → Jev: allow · block · block-if-undecidable
gateScope decides how far the semantic layer reaches, and the default is all. A denylist
can only recognise the shapes someone wrote a pattern for first: a command that uploaded a file
(curl -d @~/.ssh/id_ed25519 ...) matched nothing and ran with no judgment at all. Under all
the deterministic layer names what it can vouch for — read-only inspection, your declared safe
commands, a write inside the project to an unprotected path — and everything else is judged.
matched restores the old pattern-only behaviour. /jev-auto-mode scope all|matched changes it.
The trade is latency: a judged call costs roughly half a second (measured 193–642 ms across eleven ordinary commands), while a fast-path call costs nothing. Read-only inspection is therefore a real allowlist rather than a convenience.
An auto mode that stops for ordinary work has no reason to exist, so the intent question is
asked only about commands the deterministic layer recognised as a dangerous shape, and only a
clear "this was not requested" blocks. Measured: an unrequested mv, cp, tar, chmod +x,
or node -e is judged and allowed, while an unrequested git reset --hard, npm publish,
rm -rf, or sudo is blocked.
rm -rf build inside the repository is recognized as a scoped local deletion. A write to
.env, .git/, ~/.ssh, .pi/, .github/workflows/, or AGENTS.md is escalated even when
the path is inside the working directory.
How Jev decides
Conditions are phrased so the safe state is "yes", and each one is classified by
mode and severity:
| meaning | |
|---|---|
mode: required |
must be satisfied; the middle band follows the uncertain setting |
mode: hazard |
only a clear negative matters; the middle band is ignored |
severity: hazard |
a clear rejection always blocks |
severity: soft |
a clear rejection is cleared when the user's own request covers the call |
So intent_coverage ("is this what the user asked for?") is the permission question, and
questions like "is a secret being sent to a network endpoint" are hazard detectors that only
block when they are sure. Posting a private key is never cleared by intent; force-pushing a
feature branch the user asked for is.
Nothing is delegated to the user by default. The middle band — where the model is neither
satisfied nor rejecting — resolves to an approval (uncertain: "allow", since 0.4.1).
/jev-auto-mode uncertain deny blocks that band, and ask requests confirmation.
A valid uncertain judgment is distinct from a missing decision: no engine, timeout,
malformed response, HTTP failure, and cancellation always block, under all three settings.
Install
pi install npm:pi-jev-auto-mode
Or straight from the repository, which needs no npm account:
pi install git:github.com/jomatsu/pi-jev-auto-mode
Try it without installing:
pi -e npm:pi-jev-auto-mode
Packages are discovered in the package gallery through the
pi-package keyword.
Usage
/jev-auto-mode show status (settings, provider, model, where the credential comes from)
/jev-auto-mode on|off toggle auto mode
/jev-auto-mode login store a TypeSafe API key (verified, then saved 0600; TypeSafe only)
/jev-auto-mode logout remove the stored TypeSafe key
/jev-auto-mode policy list the policy notes
/jev-auto-mode policy edit
/jev-auto-mode policy clear
/jev-auto-mode threshold show thresholds and the last observed probability per rule
/jev-auto-mode threshold <rule> <0.5-1> set one threshold
/jev-auto-mode threshold edit pick a rule and type a value
/jev-auto-mode threshold reset [rule] restore the default threshold
/jev-auto-mode uncertain show what the middle band resolves to
/jev-auto-mode uncertain deny|ask|allow
/jev-auto-mode scope all|matched how far the semantic layer reaches
/jev-auto-mode display full|compact how decision records are drawn
/jev-auto-mode model show the Jev model and the releases your key can use
/jev-auto-mode model latest|<release> follow jev-latest (default) or pin a release
Each decision leaves a record in the transcript. display: "compact" (the default) folds an
approval into one line and a block into two (the rationale stays, because a block is something
to act on). display: "full" shows the heading, the engine line, the command, the reasons, and
the rationale. The expanded view shows every detail in either mode.
pi --jev-auto-mode start with auto mode enabled
The default semantic layer needs a TypeSafe API key. Jev is early access, so an account may be waitlisted; the gate still works without one. Its own rules keep running — read-only and user-declared safe commands pass, hard-deny shapes are blocked — but a call nothing vouches for is blocked with "Not connected to Jev" instead of being judged.
/jev-auto-mode login asks for the key, verifies it against the API (GET /v1/models), and
stores it as an owner-only file at
$PI_CODING_AGENT_DIR/secrets/jev-auto-mode-typesafe-api-key (mode 0600) — the same place Pi
keeps its own credentials, so it is never committed with a project. TYPESAFE_API_KEY takes
precedence when set, so a one-off or CI override needs no login.
Jev releases
The default model is jev-latest, which TypeSafe resolves to its newest Jev release, so the
gate follows new releases without any change. To keep judgments on one release, pin it:
/jev-auto-mode model # current model + releases available to your key, newest first
/jev-auto-mode model jev-1.13.0 # pin a release ("jevModel" in the global settings)
/jev-auto-mode model latest # back to jev-latest
A release is saved only after the API lists it for your key, so a typo cannot turn into a gate
that blocks every escalated call. JEV_AUTO_MODE_MODEL or TYPESAFE_DEFAULT_MODEL in the
environment take precedence over the setting; /jev-auto-mode status shows which one applied.
The thresholds were calibrated on one release, so re-check them after pinning another
(Tuning).
A key is only stored after the API accepts it: a typo that got saved would turn into a gate that silently blocks every escalated call. If the API cannot be reached the key is not stored either, and the command says so rather than claiming success.
Without a key the gate does not disable itself and does not allow everything: it stops the calls
it cannot judge and says it is not connected to Jev. The footer shows 🛡 jev (<scope>) while the
semantic layer is active and 🛡 jev no key (<scope>) when it is not.
Other decision providers
To use another decision backend, set JEV_AUTO_MODE_PROVIDER explicitly before starting Pi.
The default remains typesafe, even if other API keys are present. Provider selection is read
from the process environment, never a project file, and a failed request never falls back to
another provider.
JEV_AUTO_MODE_PROVIDER |
Model (JEV_AUTO_MODE_MODEL) |
Credentials / endpoint |
|---|---|---|
typesafe (default) |
jev-latest |
TYPESAFE_API_KEY or /jev-auto-mode login |
perplexity |
pplx-decider-v1-27b |
PERPLEXITY_API_KEY |
cloudflare |
clef or clef-flash |
CLOUDFLARE_AUTH_TOKEN, CLOUDFLARE_ACCOUNT_ID |
strands |
chosen by the local server (leave unset) | STRANDS_DECIDER_BASE_URL, loopback only, default http://127.0.0.1:8000 |
openai |
gpt-6-luna |
OPENAI_API_KEY |
JEV_AUTO_MODE_PROVIDER=perplexity pi
JEV_AUTO_MODE_PROVIDER=cloudflare JEV_AUTO_MODE_MODEL=clef-flash pi
JEV_AUTO_MODE_PROVIDER=strands pi
JEV_AUTO_MODE_PROVIDER=openai pi
The footer shows the active provider (🛡 perplexity (<scope>)), or 🛡 <provider> unavailable (<scope>) plus a warning with the reason when its configuration is missing or invalid; the gate then blocks
what it cannot judge, exactly as without a TypeSafe key. An OpenAI refusal is treated the same
way: no decision, so the call is blocked. Remote providers retry network errors
(including a connection lost while reading the response), 408, 429, and 5xx within the timeout
budget. Strands is never retried: requests are serialized per loopback server, and if a
dispatched request ends before its response is fully read, that endpoint is blocked for the rest
of the Pi runtime (strands_completion_unknown). Recover by restarting the Strands server and
then Pi.
The thresholds were calibrated on Jev only. See provider setup and compatibility limits for the wire contracts, the Strands recovery procedure, and validation limits. Login/logout manages the TypeSafe key only.
Tuning
The thresholds are a starting point measured on twelve fixtures with Jev, not a truth
(docs/calibration.md). They have not been calibrated on the
additional providers.
To retune them from your own work:
- Run the thing you care about. The gate records every judgment; expand the record in the transcript and read the per-condition table:
intent_coverage p=0.97 pass (t=0.80, >= 0.80)
no_outward_effect p=0.06 reject (t=0.90, <= 0.10) <- decided (cleared by the user's request)
local_scope p=0.81 pass (t=0.90, >= 0.90)
If a condition that should have passed lands in the middle band, lower its threshold. If something got through that should not have, raise it.
/jev-auto-mode thresholdshows the current value next to the last probability the model returned for that rule./jev-auto-mode threshold <rule> <value>writes the override. It takes effect immediately and persists in the global settings file.
A threshold must leave a middle band on both sides (0.5 < t <= 1): t is the probability
required to count as satisfied, and 1 - t is the probability at or below which the condition
counts as violated. Values that close one side are rejected.
{
"thresholds": {
"intent_coverage": 0.6,
"no_secret_egress": 0.995
}
}
The right fix is usually to phrase the condition better, not to move the threshold. If "should pass" and "should reject" answers overlap, the question is ambiguous.
Configuration
Global settings: $PI_CODING_AGENT_DIR/jev-auto-mode.json (default ~/.pi/agent/).
Project override: <cwd>/.pi/jev-auto-mode.json, honored only for a trusted project —
an untrusted checkout must not be able to loosen the gate that is judging it.
Policy notes: $PI_CODING_AGENT_DIR/jev-auto-mode-policy.md.
{
"enabled": true,
"timeoutMs": 4000,
"maxRetries": 1,
"safeCommands": ["uv run pytest*", "pnpm run typecheck*"],
"allowedCommands": ["rm -rf build*"],
"disallowedCommands": ["npm publish*"],
"extraProtectedPaths": [],
"maxStateCharacters": 120000,
"uncertain": "allow",
"gateScope": "all",
"display": "compact",
"thresholds": {}
}
jevModel (for example "jev-1.13.0") pins a Jev release; leave it out to follow
jev-latest. Set it with /jev-auto-mode model so the release is checked first.
Allow patterns never match a command containing shell control syntax (
;,&&,|, redirection, substitution), sols*cannot approvels && rm -rf /.Malformed values are dropped rather than defaulted, so a broken project file cannot pin a value that overrides the global layer.
There are two ways to widen the fast path, with different meanings:
Setting Effect safeCommandsRun without a decision record. For commands that are safe on your machine: uv run pytest*,npm run test*,cargo test*,go test ./...*allowedCommandsOverride a dangerous-pattern match. The override is recorded, so approving rm -rf buildby rule is visibleThe built-in safe list is not configurable and holds read-only inspection only (
git status/diff/log/show/branch,ls,pwd,rg,grep). It deliberately contains no command that executes project code: a test runner runs repository code, so declaring it safe is a decision for the machine that owns it, not a default shipped to everyone. Allow patterns never match a command containing shell control syntax, sols*cannot approvels && rm -rf /.
What leaves the machine
An escalated call sends the following to the selected provider: TypeSafe by default, OpenAI, Perplexity or Cloudflare when explicitly selected, or the loopback Strands server:
- the tool name and the bash command text (truncated),
- for
write/edit: the target path — never the file contents or the diff, - the working directory, the matched policy reason names,
- recent user messages (bounded), and your policy notes.
Obvious credentials in bash command text (*_KEY=…, Bearer …, JWTs, sk-…, ghp_…, PEM
private keys) are redacted. Recent user messages and policy notes are bounded but are not
secret-filtered, so they can contain sensitive text. Assistant output, tool output, and file
contents are never sent.
Details and the failure-mode table: docs/security.md.
Releasing
A version, a tag, and a release are cut once, when the version is published, so the tag list
matches what people can install. Unfinished work accumulates under ## Unreleased in
CHANGELOG.md and is renamed to the version at release time:
npm run check- rename
## Unreleasedto## <version>inCHANGELOG.md, bumpversioninpackage.json - commit,
git tag -a v<version>,git push --follow-tags gh release create v<version> --notes-file <(the changelog section)npm publish
Development
npm install
npm test # node:test, no network
npm run typecheck
node --experimental-strip-types scripts/calibrate.ts # real API, needs TYPESAFE_API_KEY
Layout:
| Path | Responsibility |
|---|---|
src/policy.ts |
hard-deny, user rules, dangerous patterns, protected paths, safe commands |
src/call.ts |
tool_call → judgment state (redaction, truncation, path classification) |
src/intent.ts |
recent user-authored intent only |
src/decide.ts |
the decision-engine seam (DecisionEngine) |
src/providers.ts |
explicit provider selection, credentials, model and endpoint validation |
src/decision-http.ts |
OpenAI, Perplexity, Cloudflare REST, and local Strands transport contracts |
src/jev/questions.ts |
the condition set, modes, severities, thresholds |
src/jev/availability.ts |
where the API key comes from (env or stored secret) |
src/jev/decide.ts |
probability → condition verdict → decision |
src/jev/engine.ts |
one request per call, budget guard, calibration hook |
src/jev/transport.ts |
the SDK, wrapped so failures become decisions |
src/jev/response.ts |
response re-validation (a 200 is not an answer) |
src/settings.ts |
global/project settings, policy notes, and the stored API key |
src/records.ts |
appendEntry records and their renderer |
src/ui.ts |
footer status and user-facing text |
src/extension.ts |
tool_call orchestration and command wiring |
Acknowledgements
The deterministic pattern catalogue is adapted from
@nilskluewer/pi-auto-permission-gate
(MIT), and the three-layer structure (fast paths → hard rules → classifier) follows the same
extension and Qwen Code's Auto Mode. The Jev design constraints (fail closed, two-sided
thresholds that keep the middle band meaningful, one request per judgment) come from measuring
the API directly — docs/calibration.md records the measurements and
the reasoning. Nothing here depends on a wrapper library: the Jev layer is written against the
official SDK.
License
MIT