Skip to content

czottmann/pi-automode

Repository files navigation

pi-automode

Claude Code-style auto mode for Pi.

This is a guardrail extension. It intercepts agent tool calls before execution and blocks actions that match permission deny rules, deterministic hard-deny checks, or the auto-mode classifier's block decision.

It is not a sandbox. Extensions run in the Pi process, and a determined malicious extension can do anything your user account can do. It also does not guard user ! / !! shell commands; by design, it guards agent tool calls only. Use this to reduce unsafe autonomous tool use, not as an OS security boundary.

Install

From npm:

pi install npm:@czottmann/pi-automode

From a local checkout:

pi install .

For one run from a local checkout:

pi -e ./extensions/auto-mode.ts

Commands

/automode status    # current state, rules, and classifier
/automode on        # re-enable for this session
/automode off       # disable for this session
/automode reload    # reload config from disk
/automode reset     # reset denial counters only
/automode defaults  # print the built-in rule lists
/automode config    # effective config, resolved log file path, + diagnostics
/automode denials   # denial history for this session
/automode model     # open classifier model selector and save to ~/.pi/agent/automode.json
/automode model provider/model-id # save classifier model to ~/.pi/agent/automode.json

/auto-mode is an alias.

Status line

When the Pi TUI is available, the extension renders a persistent status line:

AM● a:12 d:2 ca:5 cd:1
  • AM — auto-mode prefix; when enabled, when disabled (via config or /automode off).
  • a: — actions allowed so far (checked minus denied).
  • d: — actions denied so far, for any reason (permission rule, deterministic hard-deny, or classifier).
  • ca: / cd: — classifier decisions split into allowed vs denied. These segments appear only after the classifier has run at least once; d: counts all denials, so d: is always >= cd:.

Docs

Configuration

The extension follows Claude Code's documented config model where Pi can support it.

It reads autoMode from Pi-owned config only:

  • ~/.pi/agent/automode.json
  • .pi/automode.local.json
  • PI_AUTOMODE_SETTINGS_JSON

It deliberately does not read autoMode from shared project .pi/automode.json, because a checked-in repo should not be able to weaken auto-mode rules. Shared project config may still contribute permissions.deny and permissions.ask.

To disable pi-automode for the current project, create or edit .pi/automode.local.json:

{
  "autoMode": {
    "enabled": false
  }
}

This is project-local and should not be committed. Shared project .pi/automode.json cannot disable auto-mode.

Set a global default classifier model in ~/.pi/agent/automode.json; override it per project in .pi/automode.local.json.

classifierReasoningLevel optionally requests low, medium, high, xhigh, or max reasoning for both classifier stages. If the key is absent, pi-automode sends no reasoning preference and leaves the choice to the server. Pi AI clamps unsupported values to the nearest level supported by the selected model; a non-reasoning model resolves to off. low matches Codex Auto Review's reasoning effort and is the practical default when an explicit value is needed. Higher levels can consume the existing 512/1200-token stage limits before producing visible output, which causes the classifier to fail closed.

The setting follows the normal scalar precedence: global, then project-local, then PI_AUTOMODE_SETTINGS_JSON. Shared project .pi/automode.json cannot set it. Omitting the key at a higher-precedence scope does not clear a lower-precedence value.

Example:

{
  "autoMode": {
    "classifierModel": "provider/model-id",
    "classifierReasoningLevel": "low",
    "maxUserTranscriptTokens": 4000,
    "maxToolTranscriptTokens": 4000,
    "environment": [
      "$defaults",
      "Source control: github.example.com/acme-corp and all repos under it",
      "Trusted internal domains: *.corp.example.com, git.example.com",
      "Trusted cloud buckets: s3://acme-dev-artifacts, gs://acme-ci-cache",
      "Key internal services: staging deploy API at deploy.corp.example.com"
    ],
    "allow": ["$defaults"],
    "protectedPaths": ["$defaults"],
    "soft_deny": ["$defaults"],
    "hard_deny": [
      "$defaults",
      "Never send repository contents to third-party code-review APIs"
    ]
  },
  "permissions": {
    "deny": ["bash(rm -rf *)"],
    "ask": ["bash(git push *)"]
  }
}

maxUserTranscriptTokens and maxToolTranscriptTokens are approximate per-category budgets; both default to 4000 and accept integers of at least 32. The former maxTranscriptLines setting is no longer supported because evidence selection is token-budgeted rather than line-based.

$defaults

See Defaults and rule-list behavior for built-in environment, allow, protectedPaths, soft_deny, and hard_deny entries, plus replacement behavior when $defaults is omitted.

Observability logging

Auto mode can write a JSONL observability log next to the current Pi session file, so you can inspect decisions and classifier usage. It is off by default.

{
  "autoMode": {
    "log": {
      "enabled": true,
      "classifierIo": false
    }
  }
}

With logging enabled, the sidecar also writes ccusage-compatible entries for every classifier response. ccusage pi reports this usage as a separate -pi-automode session even when classifierIo is off.

See Observability logging for the log file location, entry schema, and the classifierIo privacy tradeoff. Run /automode config to see the resolved log file path.

Permission patterns

Permission patterns use Pi tool names, for example bash(...), write(...), edit(...), read(...). The parser accepts capitalized names like Bash(...) for convenience, but the documented form is lowercase because Pi tool names are lowercase.

What is enforced before the classifier

The extension blocks these before any allow or classifier decision:

  • permissions.deny matches
  • declined permissions.ask matches
  • shell profile writes
  • SSH authorized_keys writes
  • cron, launch agent, and system service persistence
  • TLS/certificate/auth weakening patterns
  • root, home, and system-path destructive deletes
  • edits to .pi/automode*, .pi auto-mode files, and this extension's safety-control files

Read-only Pi tools (read, grep, find, ls) are allowed after those checks. Every side-effecting action goes to the classifier, including all write and edit calls, bash, MCP, subagent, network-capable tools, and unknown tools. This keeps classifier hard-deny rules unconditional; direct file writes cannot bypass them.

Classification starts with a one-token conservative filter and runs structured review only when that filter requests it. Both stages use a classifier-specific session key and short provider cache retention where the provider supports it. Missing models, provider failures, or malformed responses block the action.

Examples

  • examples/automode.local.json: copy to .pi/automode.local.json in a project and edit the domains, buckets, and source-control org.

Known limits

Claude Code's real classifier and exact built-in rules are private. This package implements the documented precedence and configuration behavior, with a local classifier prompt and deterministic hard-deny checks.

Development

npm run check
npm test
npm pack --dry-run

The tests cover the risky parts: scoped permission matching, config-source precedence, $defaults behavior, config diagnostics, deterministic hard-deny checks, shell parsing, write/edit classifier routing, symlink-aware safety-control checks, token-budgeted transcript selection, staged classifier parsing and caching options, and hook-level blocking/allowing.

Publishing

GitHub Actions publishes the package to npm when a GitHub Release is published. The release tag must match package.json exactly, with or without a leading v (v1.0.0 and 1.0.0 both work for version 1.0.0).

The workflow uses npm Trusted Publishing, so it does not need an npm token secret. Configure this package on npm with this repository and workflow file (.github/workflows/publish.yml). The workflow builds the package, runs npm run check, and publishes with npm provenance.

Release tag must point at the version bump

The publish workflow checks out the commit the release tag points at and compares package.json against the tag name. The tag must point at a commit where package.json already carries the new version. Concretely: commit the version bump (chore: release X.Y.Z), push main, then create the GitHub release targeting that pushed commit. Creating the release before pushing the version bump — or targeting a commit that still has the old version — fails the Check release tag step with Release tag (vX.Y.Z) does not match package.json version (x.y.z).

If the tag was cut against the wrong commit, fix it by force-moving it to the version-bump commit and pushing, then trigger the workflow via gh workflow run publish.yml --ref vX.Y.Z (the release event fires on tag creation; re-running a failed release-triggered run reuses the original ref and won't pick up the moved tag).

Author

Carlo Zottmann, carlo@zottmann.dev

About

Think Claude Code's auto mode but for pi. Selectable model, sensible defaults.

Topics

Resources

License

Stars

15 stars

Watchers

0 watching

Forks

Contributors