{
  "name": "@vanillagreen/pi-hooks",
  "version": "0.12.0",
  "description": "The carrier that runs kendex's hooks under Pi: it dispatches the rendered kendex/hooks.json registry on every listener kendex maps a hook event onto, so a PreToolUse, PostToolUse, Stop, TaskCompleted or SessionStart hook fires, the bash guards kendex renders and a custom hook of your own alike, and it adds end-of-turn lint and a session-start kendex drift report. Per-hook toggles via pi-extension-manager.",
  "license": "MIT",
  "keywords": [
    "pi-package",
    "pi",
    "coding-agent",
    "hooks",
    "safety",
    "lint"
  ],
  "pi": {
    "extensions": [
      "./extensions/hooks.ts"
    ]
  },
  "scripts": {
    "test": "bun test ./tests"
  },
  "kendex": {
    "extensionManager": {
      "displayName": "Hooks",
      "settings": [
        {
          "key": "enabled",
          "label": "Enable hooks",
          "description": "Master toggle for all pi-hooks safety checks. Disable to make the extension inert without uninstalling.",
          "type": "boolean",
          "default": true,
          "category": "General",
          "apply": "live"
        },
        {
          "key": "blockBareCd",
          "label": "Block bare cd",
          "description": "Run the rendered hooks/block-bare-cd.sh on every bash tool call. It refuses a command with a line that is only a bare `cd /path` or `cd` (no subshell, no `&&` chain). Pi runs each command in a fresh shell, so the cd changes nothing here; the refusal keeps the one form that is right on every harness, `(cd /path && command)`.",
          "type": "boolean",
          "default": true,
          "category": "Bash",
          "apply": "live"
        },
        {
          "key": "blockRepoCopy",
          "label": "Block repo copy into scratch",
          "description": "Run the rendered hooks/block-repo-copy.sh on every bash tool call. One regex over the raw command decides, in the order the words stand: a copy verb (`cp`, `rsync`, `tar`, `git clone`), a source word whose last path component IS `.git` or `target` — both edges are tested, so a `\u2026/bar.git` clone URL and a `build-target` directory are not it — and a destination under `/tmp`, `/var/tmp` or `$TMPDIR`. Temp roots are commonly RAM-backed tmpfs, where such a copy fills the filesystem and every process writing there fails with ENOSPC. All three parts are required, so an expensive tree copied elsewhere, an ordinary directory copied into scratch, and one file out of a build tree all pass. Nothing is resolved, expanded or stat-ed: a source reached through a variable, a repository named only by its working-tree path, and a `tar -czf DEST SRC` that spells its destination first are not seen, while a copy spelled inside a quoted string is refused as the copy it is not.",
          "type": "boolean",
          "default": true,
          "category": "Bash",
          "apply": "live"
        },
        {
          "key": "preCommitCheck",
          "label": "Pre-commit gate",
          "description": "Run the rendered hooks/pre-commit-check.sh on every bash tool call. On a command whose whitespace-separated words hold a `git` word and a later `commit` word, it defers to the working directory's git hooks when kendex armed them (`kendex guard install`: pre-commit and commit-msg both executable and marked, `core.hooksPath` unset), so git validates the commit exactly once. It refuses a commit carrying a word that would skip those hooks — the no-verify flag, a short cluster holding its letter, or a word carrying a `core.hooksPath` key (an attached -c value, the value after a bare -c, a --config-env, a `git config` argument, a `GIT_CONFIG_*` assignment) — since git would skip commit-msg too, and a commit in a repository nothing armed, naming `kendex guard install`. It never runs a check of its own. It reads no shell. One rewrite runs first: every metacharacter bash(1) lists that is not whitespace (`| & ; ( ) < >`) becomes a space, so a word bash would have separated is separated here too, and nothing is deleted. A word is seen only where the command already spells it, so a bypass the shell would join, unquote or expand into the word is not seen here and reaches git, which skips its armed hooks. The reading runs the other way: a `git` word, a `commit` word and a bypass word the split leaves standing each count wherever they stand, a commit message, a heredoc body and a comment tail included, and quoting does not change that on its own. Git's armed hooks are the control either way. Gates the working directory only: a commit aimed at another repository is that repository's own hook's to gate.",
          "type": "boolean",
          "default": true,
          "category": "Bash",
          "apply": "live"
        },
        {
          "key": "taskCompletedCheck",
          "label": "End-of-turn clippy",
          "description": "At turn end, if any .rs files were touched during the turn, run a final `cargo clippy --workspace --all-targets -- -D warnings` and steer the result to the agent, so a headless session reads it in the turn that follows; an interactive session also gets a UI notification. Every failing turn reports, an unchanged failure included; a turn that touched no .rs file runs no clippy and reports nothing. A run that established nothing — no workspace root, a timeout, a clippy failure printing no error line — says so rather than passing as clean. Pi has no equivalent of Claude Code's `TaskCompleted` block semantics, so this is advisory.",
          "type": "boolean",
          "default": true,
          "category": "Rust",
          "apply": "live"
        },
        {
          "key": "sessionDriftCheck",
          "label": "Session-start drift report",
          "description": "On a fresh session start (startup, new, fork — not resume or reload), run `kendex check --quiet` in the background and hand the agent the drift report: outdated items (`kendex refresh`), items removed upstream (`kendex remove <name>`), unreachable sources, and packages not yet evaluated against their sources (a background refresh settles them). Silent when the install is current; one line when no kendex binary is on PATH, and one line naming the reason if the check itself fails unexpectedly. Never blocks startup. Informational only — never installs or removes anything and never touches the project's git; kendex's own source caches under ~/.kendex/cache may be fetched at most once per TTL. Mirrors hooks/session-drift-check.sh.",
          "type": "boolean",
          "default": true,
          "category": "Session",
          "apply": "live"
        },
        {
          "key": "driftCheckTimeoutMs",
          "label": "Drift check timeout",
          "description": "Maximum milliseconds the session-start `kendex check` may run before it is abandoned. Matches the `timeout: 30` declared in hooks/session-drift-check.sh.",
          "type": "number",
          "default": 30000,
          "category": "Session",
          "apply": "live"
        },
        {
          "key": "clippyTimeoutMs",
          "label": "Clippy timeout",
          "description": "Maximum milliseconds the whole end-of-turn check may take: a quarter of it, capped at 5s, goes to the `cargo metadata` workspace lookup and the rest to `cargo clippy`, each abandoned past its share so the turn can continue. Raise for slow workspaces.",
          "type": "number",
          "default": 30000,
          "category": "Rust",
          "apply": "live"
        }
      ]
    }
  },
  "peerDependencies": {
    "@earendil-works/pi-coding-agent": "*"
  },
  "files": [
    "extensions/",
    "README.md",
    "package.json"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vanillagreencom/kendex.git",
    "directory": "pi-extensions/pi-hooks"
  },
  "homepage": "https://github.com/vanillagreencom/kendex/tree/main/pi-extensions/pi-hooks",
  "bugs": {
    "url": "https://github.com/vanillagreencom/kendex/issues"
  },
  "author": "vanillagreen",
  "publishConfig": {
    "access": "public"
  },
  "engines": {
    "node": ">=22.19.0"
  },
  "peerDependenciesMeta": {
    "@earendil-works/pi-coding-agent": {
      "optional": true
    }
  }
}
