You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/10/16 07:12:35 UTC

(magpie) branch main updated: Add JetBrains Junie harness support (agent-guard --junie adapter, adapter guide) (#1666)

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git


The following commit(s) were added to refs/heads/main by this push:
     new 6c24f30c Add JetBrains Junie harness support (agent-guard --junie adapter, adapter guide) (#1666)
6c24f30c is described below

commit 6c24f30cbca684234fd6caa028bc7e4ede908b90
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Fri Oct 16 08:12:30 2026 +0100

    Add JetBrains Junie harness support (agent-guard --junie adapter, adapter guide) (#1666)
    
    Generated-by: GitHub Copilot (Claude Sonnet 5.5)
---
 CONTRIBUTING.md                                    |   6 +-
 docs/adapters/README.md                            |   2 +
 docs/adapters/junie.md                             | 104 +++++++++++++++++++++
 docs/adapters/registry.md                          |   2 +-
 docs/setup/marketplace.md                          |   8 +-
 docs/vendor-neutrality.md                          |   4 +-
 .../tools/agent-guard/src/agent_guard/__init__.py  |  44 +++++++++
 plugins/magpie-setup/skills/setup/agents.md        |   2 +-
 .../setup/setup_preflight/isolated_fingerprint.py  |   2 +-
 tools/agent-guard/README.md                        |  17 +++-
 tools/agent-guard/src/agent_guard/__init__.py      |  44 +++++++++
 tools/agent-guard/tests/test_junie.py              | 104 +++++++++++++++++++++
 .../src/vendor_neutrality_score/__init__.py        |   1 +
 13 files changed, 326 insertions(+), 14 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a64afb8d..9bb4e165 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -399,13 +399,13 @@ authoritative agent harness matrix. The current state per harness:
 | GitHub Copilot CLI + Coding Agent | Standalone CLI and server-side Coding Agent; Draft PR boundary and clean-env isolation — see the [harness guide](docs/adapters/copilot.md) | [#318](https://github.com/apache/magpie/issues/318) |
 | Goose (Block) | Open-source agent CLI; reads `.agents/skills/` natively, developer tool execution, and recipe workflows — see the [harness guide](docs/adapters/goose.md) | [#319](https://github.com/apache/magpie/issues/319) |
 | Amazon Q Developer CLI | Not yet ported | [#320](https://github.com/apache/magpie/issues/320) |
-| JetBrains Junie | Not yet ported | [#321](https://github.com/apache/magpie/issues/321) |
+| JetBrains Junie (CLI + IDE plugin) | Reads `.agents/skills/` and `AGENTS.md` natively; `PreToolUse` action guard (hooks are Early Access) — see the [harness guide](docs/adapters/junie.md) | [#321](https://github.com/apache/magpie/issues/321) |
 | OpenHands | Not yet ported | [#322](https://github.com/apache/magpie/issues/322) |
 
 The table lists **harnesses** — the agent that reads the skills — not the
 editors they run in. A JetBrains IDE, for instance, hosts an agent rather than
-being one: `JetBrains Junie` above is JetBrains' *own* agent and is genuinely
-not ported, but running Claude Code inside IntelliJ IDEA or PyCharm is the
+being one: `JetBrains Junie` above is JetBrains' *own* agent, ported separately from
+running Claude Code inside IntelliJ IDEA or PyCharm is the
 fully-supported Claude Code row, with nothing extra to install. See
 [the JetBrains section of `marketplace.md`](docs/setup/marketplace.md#jetbrains-ides-intellij-idea-pycharm-goland-).
 
diff --git a/docs/adapters/README.md b/docs/adapters/README.md
index 98cf1cea..694f9c69 100644
--- a/docs/adapters/README.md
+++ b/docs/adapters/README.md
@@ -35,6 +35,7 @@ One page per supported agentic harness, each declaring
 - [**Gemini CLI**](gemini.md) — extension install, `BeforeTool` guard, tool
   sandboxing and policies. Experimental.
 - [**Grok Build**](grok.md) — `PreToolUse` action-guard integration.
+- [**JetBrains Junie**](junie.md) — native `.agents/skills/` and `AGENTS.md`, plus an Early Access `PreToolUse` action guard.
 - [**Goose**](goose.md) — Block's open-source agent CLI and desktop environment.
 - [**Kiro CLI**](kiro.md) — per-skill installs, no marketplace; guard on
   `preToolUse`.
@@ -54,6 +55,7 @@ reaches six of ten.
 | **OpenCode** | `opencode-iso` | from the OS-level sandbox | ✅ `tool.execute.before` |
 | **Kiro CLI** | `kiro-iso` | from the OS-level sandbox | ✅ `preToolUse` |
 | **Grok Build** | ❌ **none** | ❌ **none** | ✅ `PreToolUse` |
+| **JetBrains Junie** | `junie --sandbox` (srt; MCP and hooks unsandboxed) | `/brave` levels and `~/.junie/allowlist.json` | ✅ `PreToolUse` (Early Access) |
 | **OpenAI Codex CLI** | `agent-iso codex` | Codex's own sandbox and exec policy, statically validated by [sandbox-lint](../../tools/sandbox-lint/README.md) | ❌ **none** |
 | **Cursor** | `agent-iso cursor` | Cursor's own policy | ❌ **none** |
 | **Goose (Block)** | `agent-iso goose` | Goose developer mode / approval prompts | ❌ **none** |
diff --git a/docs/adapters/junie.md b/docs/adapters/junie.md
new file mode 100644
index 00000000..9f47d480
--- /dev/null
+++ b/docs/adapters/junie.md
@@ -0,0 +1,104 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [JetBrains Junie](#jetbrains-junie)
+  - [Harness contract](#harness-contract)
+  - [Installation](#installation)
+  - [Action guard](#action-guard)
+  - [Verify](#verify)
+  - [Known limitations](#known-limitations)
+  - [Upstream references](#upstream-references)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# JetBrains Junie
+
+**Capability:** platform:harness
+
+Junie is JetBrains' own coding agent, available as a CLI and as a JetBrains IDE plugin.
+It is a different harness from Claude Code running inside a JetBrains IDE, which needs nothing extra (see [`marketplace.md`](../setup/marketplace.md#jetbrains-ides-intellij-idea-pycharm-goland-)).
+Tracking issue: [#321](https://github.com/apache/magpie/issues/321).
+
+## Harness contract
+
+| Surface | Junie contract | Magpie integration |
+|---|---|---|
+| Skills | Open Agent Skills format; reads `<project>/.agents/skills/` (trusted projects), `~/.agents/skills/`, `<project>/.junie/skills/`, `~/.junie/skills/` | The `universal` registry row; no Junie-specific relay |
+| Invoke a skill | Automatically, as `/<skill-name>`, or as `$<skill-name>` | Same skill files as every other harness |
+| Instructions | `AGENTS.md` in the project root, `.junie/rules/*.md`, `~/.junie/AGENTS.md` | `AGENTS.md` is read as is |
+| MCP | `.junie/mcp/mcp.json` (project) with the usual `mcpServers` shape | Adopter-owned; Magpie writes none |
+| Action guard | `PreToolUse` hook in `~/.junie/config.json` (Early Access) | `agent-guard --junie` |
+| Sandbox | `junie --sandbox` (sandbox-runtime, macOS and Linux) | Not managed by Magpie |
+| Approvals | `/brave` levels and `~/.junie/allowlist.json` | Not managed by Magpie |
+
+## Installation
+
+Install an adopted project with the `universal` target, which is the default.
+Junie loads `.agents/skills/` only in a trusted project; trust it from Junie itself, Magpie never grants it.
+
+## Action guard
+
+Junie's hook payload is Claude-shaped (`tool_name`, `tool_input.command`) but its block semantics differ.
+Junie honours exit code 2 and a top-level `decision`, and ignores Claude's `hookSpecificOutput`.
+The no-argument `agent-guard` would route the payload to the Claude adapter, exit 0, and let every command through.
+Hooks must therefore pass `--junie`.
+
+Add to `~/.junie/config.json` (or a file passed with `--config-location`):
+
+```json
+{
+  "hooks": {
+    "PreToolUse": [
+      {
+        "matcher": "Bash",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "python3 <framework>/tools/agent-guard/src/agent_guard/__init__.py --junie",
+            "timeout": 30
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+Replace `<framework>` with the absolute path of the checkout, or of `.apache-magpie/` for a snapshot adopter.
+Junie hooks live in user configuration, so Magpie does not write the file; the adopter adds the entry.
+
+The adapter blocks with exit 2, the denial reason on stderr, and a `{"decision": "deny", "reason": ...}` object on stdout.
+Malformed events, other tools, and allowed commands exit 0 silently.
+Other non-zero exits only warn in Junie and let the tool run, so the adapter never uses them.
+
+## Verify
+
+1. Confirm the hook is listed in `~/.junie/config.json` and the command path exists.
+2. Run `git status` through Junie and confirm it runs.
+3. Run `git commit --no-verify --dry-run` and confirm Junie reports a block from `agent-guard[no-verify]`.
+4. Run `echo '{"tool_name":"Bash","tool_input":{"command":"git commit --no-verify"}}' | python3 <framework>/tools/agent-guard/src/agent_guard/__init__.py --junie; echo $?` and expect exit 2.
+
+A static check of the config does not replace the live denial probe.
+
+## Known limitations
+
+- Hooks are in Junie's Early Access Program; the schema may change, so re-verify after upgrades.
+- The guard covers shell commands only.
+  Native file tools, MCP servers, and hooks themselves are outside it, and outside `junie --sandbox` too.
+- The sandbox policy is the global `~/.junie/sandbox.json`; a project-local one is ignored, and network is unrestricted unless `allowedDomains` is set.
+- The payload shape was taken from the Junie documentation, not a captured live event.
+  Capture one and add it to `tools/agent-guard/tests/test_junie.py` when a build is available.
+- There is no spec-loop runner profile for Junie yet.
+
+## Upstream references
+
+- Junie skills: https://junie.jetbrains.com/docs/agent-skills.html
+- Junie hooks: https://junie.jetbrains.com/docs/junie-cli-hooks.html
+- Junie sandbox: https://junie.jetbrains.com/docs/junie-cli-sandbox.html
diff --git a/docs/adapters/registry.md b/docs/adapters/registry.md
index 0be51837..6c28f40a 100644
--- a/docs/adapters/registry.md
+++ b/docs/adapters/registry.md
@@ -56,7 +56,7 @@ extension point = a documented, labelled slot with a tracking issue.
 | [`tools/vcs`](../../tools/vcs/) | Git, Mercurial, Fossil, Subversion | Jujutsu [\#603](https://github.com/apache/magpie/issues/603), Perforce [\#605](https://github.com/apache/magpie/issues/605) |
 | Forge / tracker | [`github`](../../tools/github/), [`forgejo`](../../tools/forgejo/) `partial` foundation, [`jira`](../../tools/jira/), [`bitbucket`](../../tools/bitbucket/) `partial-read-only` foundation, [`sourcehut`](../../tools/sourcehut/), [`fossil`](../../tools/fossil/), [`gitlab`](../../tools/gitlab/) `partial-read-only` foundation | Forgejo/Gitea [\#310](https://github.com/apache/magpie/issues/310), Pagure [\#312](https://github.com/apache/magpie/issues/312), deeper Bitbucket/J [...]
 | [`tools/chat`](../../tools/chat/) | [`chat-slack`](../../tools/chat-slack/), [`chat-discord`](../../tools/chat-discord/) | Matrix [\#309](https://github.com/apache/magpie/issues/309), Zulip [\#308](https://github.com/apache/magpie/issues/308) |
-| Agent harness | Claude Code, [Codex](codex.md) `experimental` ([#313](https://github.com/apache/magpie/issues/313)), [Gemini CLI](gemini.md) `experimental` ([#314](https://github.com/apache/magpie/issues/314)), [Local LLM (Ollama / llama.cpp / vLLM)](local-llm.md) ([#315](https://github.com/apache/magpie/issues/315)), [Cursor](cursor.md) ([#316](https://github.com/apache/magpie/issues/316)), [Goose](goose.md) `guide only` ([#319](https://github.com/apache/magpie/issues/319)), [Aider](a [...]
+| Agent harness | Claude Code, [Codex](codex.md) `experimental` ([#313](https://github.com/apache/magpie/issues/313)), [Gemini CLI](gemini.md) `experimental` ([#314](https://github.com/apache/magpie/issues/314)), [Local LLM (Ollama / llama.cpp / vLLM)](local-llm.md) ([#315](https://github.com/apache/magpie/issues/315)), [Cursor](cursor.md) ([#316](https://github.com/apache/magpie/issues/316)), [Goose](goose.md) `guide only` ([#319](https://github.com/apache/magpie/issues/319)), [Aider](a [...]
 | Security cross-ref | [`tools/osv`](../../tools/osv/) | — |
 | [`tools/typed-decision`](../../tools/typed-decision/) | TypeSafe Jev API, in-tree local provider (Ollama / llama.cpp / vLLM) ([#1431](https://github.com/apache/magpie/issues/1431)) | — |
 | [`tools/people`](../../tools/people/) | [`github`](../../tools/github/), [`jira`](../../tools/jira/) (profile lookup) | GitLab, Forgejo, Bitbucket |
diff --git a/docs/setup/marketplace.md b/docs/setup/marketplace.md
index 152b4ad5..9738491d 100644
--- a/docs/setup/marketplace.md
+++ b/docs/setup/marketplace.md
@@ -473,11 +473,9 @@ elsewhere is a separate project and installs separately.
 > [!NOTE]
 > This is about running *Claude Code* (or another agent with a JetBrains
 > plugin) inside a JetBrains IDE. **JetBrains' own agent, Junie, is a separate
-> harness port** — tracked as
-> [#321](https://github.com/apache/magpie/issues/321) and listed *Not yet
-> ported* in [`CONTRIBUTING.md`](../../CONTRIBUTING.md) and
-> [`vendor-neutrality.md`](../vendor-neutrality.md). Junie does not read
-> Magpie's skills today.
+> harness** — see the [Junie guide](../adapters/junie.md)
+> ([#321](https://github.com/apache/magpie/issues/321)). It reads the
+> canonical `.agents/skills/` path, so no marketplace plugin is involved.
 
 ### Not supported
 
diff --git a/docs/vendor-neutrality.md b/docs/vendor-neutrality.md
index 39730a6e..98102043 100644
--- a/docs/vendor-neutrality.md
+++ b/docs/vendor-neutrality.md
@@ -348,7 +348,6 @@ Draft PR review gating; see the
 The remaining extension points are
 already open, labelled `good first issue`:
 [Amazon Q](https://github.com/apache/magpie/issues/320),
-[JetBrains Junie](https://github.com/apache/magpie/issues/321),
 [OpenHands](https://github.com/apache/magpie/issues/322).
 The step-by-step wiring recipe for any new runtime is
 [`docs/adapters/add-a-harness.md`](adapters/add-a-harness.md).
@@ -620,7 +619,7 @@ Organization scope (declared, orthogonal to vendor): ASF = 16, agnostic = 64.
 | Substrate tool | Substrate | Harness support | Verdict |
 |---|---|---|---|
 | `adversarial-review` | review | any | ✅ agnostic |
-| `agent-guard` | action-guard | Claude Code, Codex, Copilot, Gemini CLI, Grok, Kiro, OpenCode | ✅ portable |
+| `agent-guard` | action-guard | Claude Code, Codex, Copilot, Gemini CLI, Grok, Junie, Kiro, OpenCode | ✅ portable |
 | `agent-isolation` | sandbox | any | ✅ agnostic |
 | `container-gateway` | sandbox | any | ✅ agnostic |
 | `contributor-metrics` | analytics | any | ✅ agnostic |
@@ -660,6 +659,7 @@ Harness → substrate tools it supports:
 - **Cursor** (2): `sandbox-lint`, `spec-loop`
 - **Gemini CLI** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
 - **Grok** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
+- **Junie** (1): `agent-guard`
 - **Kiro** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
 - **OpenCode** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
 - **any harness** (29): `adversarial-review`, `agent-isolation`, `container-gateway`, `contributor-metrics`, `dashboard-generator`, `dev`, `egress-gateway`, `maven-artifact-verify`, `permission-audit`, `pilot-report-validator`, `pr-management`, `preflight-audit`, `privacy-llm`, `probe-templates`, `release-config`, `release-verify`, `reproducible-archive`, `security-tracker-stats-dashboard`, `setup-preflight`, `skill-and-tool-validator`, `skill-evals`, `skill-reconciler-diff`, `skill-toke [...]
diff --git a/plugins/magpie-agent-guard/tools/agent-guard/src/agent_guard/__init__.py b/plugins/magpie-agent-guard/tools/agent-guard/src/agent_guard/__init__.py
index 555b41bb..05cb7691 100644
--- a/plugins/magpie-agent-guard/tools/agent-guard/src/agent_guard/__init__.py
+++ b/plugins/magpie-agent-guard/tools/agent-guard/src/agent_guard/__init__.py
@@ -35,6 +35,8 @@ pre-tool hook to/from the core, so every wired harness enforces one rule set:
 * :func:`grok_main` — Grok Build ``PreToolUse`` adapter (``--grok`` or
   automatic no-argument payload-shape detection): reads the documented
   camelCase event and emits Grok's JSON allow/deny decision.
+* :func:`junie_main` — Junie CLI ``PreToolUse`` hook (``--junie``): reads the
+  snake_case event and blocks with a ``decision`` JSON and exit 2.
 * :func:`check_main` — Harness-neutral check-only entry point (``--check``):
   takes the command as remaining CLI args, exits ``DENY_EXIT`` with the reason
   on stdout on a deny, ``ALLOW_EXIT`` silently on allow, ``USAGE_EXIT`` when no
@@ -1633,6 +1635,45 @@ def grok_main() -> int:
     return _grok_event(event)
 
 
+def _junie_event(event: object) -> int:
+    """Handle one parsed Junie CLI ``PreToolUse`` event."""
+    if not isinstance(event, dict) or event.get("tool_name") != "Bash":
+        return ALLOW_EXIT
+    tool_input = event.get("tool_input")
+    if not isinstance(tool_input, dict):
+        return ALLOW_EXIT
+    command = tool_input.get("command")
+    if not isinstance(command, str):
+        return ALLOW_EXIT
+
+    cwd = event.get("cwd")
+    reason = dispatch(command, cwd if isinstance(cwd, str) else None)
+    if reason:
+        json.dump({"decision": "deny", "reason": reason}, sys.stdout)
+        sys.stdout.write("\n")
+        sys.stderr.write(reason + "\n")
+        return DENY_EXIT
+
+    return ALLOW_EXIT
+
+
+def junie_main() -> int:
+    """Translate Junie CLI ``PreToolUse`` input to the shared guard core.
+
+    Junie sends snake_case ``tool_name`` / ``tool_input`` like Claude Code but
+    does not honour Claude's ``hookSpecificOutput`` deny: it reads a top-level
+    ``decision`` and blocks unconditionally on exit code 2. The adapter emits
+    both, so the block holds whichever the runtime checks first. It is selected
+    explicitly with ``--junie`` because the no-argument shape sniff would route
+    this payload to the Claude adapter and fail open.
+    """
+    try:
+        event = json.load(sys.stdin)
+    except (json.JSONDecodeError, ValueError):
+        return ALLOW_EXIT
+    return _junie_event(event)
+
+
 def check_main(argv: list[str]) -> int:
     """Harness-neutral check-only entry point (``--check``).
 
@@ -1758,6 +1799,7 @@ def cli(argv: list[str] | None = None) -> int:
     ``--copilot`` → the Copilot CLI adapter (:func:`copilot_main`).
     ``--codex`` → the Codex native hook adapter (:func:`codex_main`).
     ``--grok`` → the Grok Build adapter (:func:`grok_main`).
+    ``--junie`` → the Junie CLI adapter (:func:`junie_main`).
     ``--check <cmd…>`` → harness-neutral check-only (:func:`check_main`).
     ``--exec <cmd…>`` → harness-neutral check-then-exec (:func:`exec_main`).
 
@@ -1778,6 +1820,8 @@ def cli(argv: list[str] | None = None) -> int:
         return copilot_main()
     if args and args[0] == "--grok":
         return grok_main()
+    if args and args[0] == "--junie":
+        return junie_main()
     if not args:
         try:
             event = json.load(sys.stdin)
diff --git a/plugins/magpie-setup/skills/setup/agents.md b/plugins/magpie-setup/skills/setup/agents.md
index 2dfd4221..a8fc67e2 100644
--- a/plugins/magpie-setup/skills/setup/agents.md
+++ b/plugins/magpie-setup/skills/setup/agents.md
@@ -22,7 +22,7 @@ go"*, never as *"what files contain"*.
 
 | Target id | Project skills dir | Kind | Reads it |
 |---|---|---|---|
-| `universal` | `.agents/skills/` | universal **(canonical)** | Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, Cline, Zed, Warp, Amp, and the rest of the cluster that converged on the shared path |
+| `universal` | `.agents/skills/` | universal **(canonical)** | Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, Junie, OpenCode, Cline, Zed, Warp, Amp, and the rest of the cluster that converged on the shared path |
 | `claude-code` | `.claude/skills/` | native (relay) | Claude Code |
 | `github` | `.github/skills/` | native (relay) | GitHub's skill loader |
 | `windsurf` | `.windsurf/skills/` | native (relay) | Windsurf |
diff --git a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py
index 402e0c84..5ef2c1f3 100644
--- a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py
+++ b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py
@@ -21,4 +21,4 @@ The isolated-setup fingerprint of the framework this package shipped with,
 for installs that do not carry the framework source (see `isolated.py`).
 """
 
-FRAMEWORK_FINGERPRINT = "sha256:62a4cc7092dfcad7"
+FRAMEWORK_FINGERPRINT = "sha256:b510123a49eb72f3"
diff --git a/tools/agent-guard/README.md b/tools/agent-guard/README.md
index 3849c039..8b2123b7 100644
--- a/tools/agent-guard/README.md
+++ b/tools/agent-guard/README.md
@@ -18,6 +18,7 @@
     - [Gemini CLI](#gemini-cli)
     - [Copilot CLI](#copilot-cli)
     - [Grok](#grok)
+    - [Junie CLI](#junie-cli)
     - [Harness-neutral path (any runtime)](#harness-neutral-path-any-runtime)
   - [Contributing guards](#contributing-guards)
   - [Tests](#tests)
@@ -31,7 +32,7 @@
 
 **Capability:** substrate:action-guard
 
-**Harness:** Claude Code, Codex, OpenCode, Kiro, Gemini CLI, Copilot, Grok
+**Harness:** Claude Code, Codex, OpenCode, Kiro, Gemini CLI, Copilot, Grok, Junie
 
 A deterministic pre-execution guard dispatcher. It inspects every shell command
 **before it runs** and **denies** the ones that would break a hard framework
@@ -60,6 +61,9 @@ so every wired harness enforces an identical rule set from one source of truth:
   Source/snapshot project hooks use `--grok`; marketplace plugin hooks are
   selected automatically from Grok's incoming hook payload shape.
   See [Grok](#grok).
+- **Junie CLI** — a `PreToolUse` hook (Early Access) on the `Bash` matcher,
+  using `--junie` (exit `2` plus a `decision` JSON and a stderr reason).
+  See [Junie CLI](#junie-cli).
 - **Any other runtime** — the `--check` and `--exec` CLI modes let any
   harness or shell wrapper enforce guard rules without a harness-specific hook
   adapter. See [Harness-neutral path (any runtime)](#harness-neutral-path-any-runtime).
@@ -409,6 +413,17 @@ together.
 
 See the [Grok adapter contract](../../docs/adapters/grok.md).
 
+### Junie CLI
+
+Junie's `PreToolUse` payload is Claude-shaped (`tool_name`, `tool_input.command`),
+so the no-argument dispatcher would route it to the Claude adapter,
+whose `hookSpecificOutput` deny Junie ignores and exit 0 would let the command run.
+Junie hooks must therefore invoke `--junie` explicitly.
+The adapter blocks with exit `2`, the one signal Junie honours regardless of output,
+and also prints a `decision` JSON and the reason on stderr.
+Other non-zero exits only warn in Junie, so the adapter never uses them.
+See [the Junie adapter](../../docs/adapters/junie.md).
+
 ### Harness-neutral path (any runtime)
 
 For runtimes without a wired pre-tool hook adapter, the engine ships two
diff --git a/tools/agent-guard/src/agent_guard/__init__.py b/tools/agent-guard/src/agent_guard/__init__.py
index 555b41bb..05cb7691 100644
--- a/tools/agent-guard/src/agent_guard/__init__.py
+++ b/tools/agent-guard/src/agent_guard/__init__.py
@@ -35,6 +35,8 @@ pre-tool hook to/from the core, so every wired harness enforces one rule set:
 * :func:`grok_main` — Grok Build ``PreToolUse`` adapter (``--grok`` or
   automatic no-argument payload-shape detection): reads the documented
   camelCase event and emits Grok's JSON allow/deny decision.
+* :func:`junie_main` — Junie CLI ``PreToolUse`` hook (``--junie``): reads the
+  snake_case event and blocks with a ``decision`` JSON and exit 2.
 * :func:`check_main` — Harness-neutral check-only entry point (``--check``):
   takes the command as remaining CLI args, exits ``DENY_EXIT`` with the reason
   on stdout on a deny, ``ALLOW_EXIT`` silently on allow, ``USAGE_EXIT`` when no
@@ -1633,6 +1635,45 @@ def grok_main() -> int:
     return _grok_event(event)
 
 
+def _junie_event(event: object) -> int:
+    """Handle one parsed Junie CLI ``PreToolUse`` event."""
+    if not isinstance(event, dict) or event.get("tool_name") != "Bash":
+        return ALLOW_EXIT
+    tool_input = event.get("tool_input")
+    if not isinstance(tool_input, dict):
+        return ALLOW_EXIT
+    command = tool_input.get("command")
+    if not isinstance(command, str):
+        return ALLOW_EXIT
+
+    cwd = event.get("cwd")
+    reason = dispatch(command, cwd if isinstance(cwd, str) else None)
+    if reason:
+        json.dump({"decision": "deny", "reason": reason}, sys.stdout)
+        sys.stdout.write("\n")
+        sys.stderr.write(reason + "\n")
+        return DENY_EXIT
+
+    return ALLOW_EXIT
+
+
+def junie_main() -> int:
+    """Translate Junie CLI ``PreToolUse`` input to the shared guard core.
+
+    Junie sends snake_case ``tool_name`` / ``tool_input`` like Claude Code but
+    does not honour Claude's ``hookSpecificOutput`` deny: it reads a top-level
+    ``decision`` and blocks unconditionally on exit code 2. The adapter emits
+    both, so the block holds whichever the runtime checks first. It is selected
+    explicitly with ``--junie`` because the no-argument shape sniff would route
+    this payload to the Claude adapter and fail open.
+    """
+    try:
+        event = json.load(sys.stdin)
+    except (json.JSONDecodeError, ValueError):
+        return ALLOW_EXIT
+    return _junie_event(event)
+
+
 def check_main(argv: list[str]) -> int:
     """Harness-neutral check-only entry point (``--check``).
 
@@ -1758,6 +1799,7 @@ def cli(argv: list[str] | None = None) -> int:
     ``--copilot`` → the Copilot CLI adapter (:func:`copilot_main`).
     ``--codex`` → the Codex native hook adapter (:func:`codex_main`).
     ``--grok`` → the Grok Build adapter (:func:`grok_main`).
+    ``--junie`` → the Junie CLI adapter (:func:`junie_main`).
     ``--check <cmd…>`` → harness-neutral check-only (:func:`check_main`).
     ``--exec <cmd…>`` → harness-neutral check-then-exec (:func:`exec_main`).
 
@@ -1778,6 +1820,8 @@ def cli(argv: list[str] | None = None) -> int:
         return copilot_main()
     if args and args[0] == "--grok":
         return grok_main()
+    if args and args[0] == "--junie":
+        return junie_main()
     if not args:
         try:
             event = json.load(sys.stdin)
diff --git a/tools/agent-guard/tests/test_junie.py b/tools/agent-guard/tests/test_junie.py
new file mode 100644
index 00000000..388106ae
--- /dev/null
+++ b/tools/agent-guard/tests/test_junie.py
@@ -0,0 +1,104 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for the Junie CLI PreToolUse adapter."""
+
+from __future__ import annotations
+
+import io
+import json
+
+import pytest
+
+import agent_guard
+
+
+def _feed(monkeypatch: pytest.MonkeyPatch, payload: object) -> None:
+    text = payload if isinstance(payload, str) else json.dumps(payload)
+    monkeypatch.setattr("sys.stdin", io.StringIO(text))
+
+
+def _event(command: object = "git status", tool: str = "Bash") -> dict[str, object]:
+    return {
+        "hook_event_name": "PreToolUse",
+        "tool_name": tool,
+        "tool_input": {"command": command, "run_in_background": False, "timeout": 30},
+    }
+
+
+def test_denied_command_exits_2_with_reason_on_stderr_and_decision(
+    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+) -> None:
+    monkeypatch.setattr(agent_guard, "dispatch", lambda command, cwd=None: "blocked by test")
+    _feed(monkeypatch, _event("git push"))
+
+    assert agent_guard.junie_main() == agent_guard.DENY_EXIT == 2
+
+    captured = capsys.readouterr()
+    assert json.loads(captured.out) == {"decision": "deny", "reason": "blocked by test"}
+    assert captured.err == "blocked by test\n"
+
+
+def test_allowed_command_is_silent(
+    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+) -> None:
+    monkeypatch.setattr(agent_guard, "dispatch", lambda command, cwd=None: None)
+    _feed(monkeypatch, _event())
+
+    assert agent_guard.junie_main() == agent_guard.ALLOW_EXIT
+
+    captured = capsys.readouterr()
+    assert captured.out == ""
+    assert captured.err == ""
+
+
+def test_cwd_is_forwarded_when_a_string(monkeypatch: pytest.MonkeyPatch) -> None:
+    seen: list[object] = []
+    monkeypatch.setattr(agent_guard, "dispatch", lambda command, cwd=None: seen.append(cwd))
+    event = _event()
+    event["cwd"] = "/work"
+    _feed(monkeypatch, event)
+
+    agent_guard.junie_main()
+
+    assert seen == ["/work"]
+
+
+@pytest.mark.parametrize(
+    "payload",
+    [
+        "not json",
+        "[]",
+        _event(tool="Read"),
+        {"tool_name": "Bash"},
+        {"tool_name": "Bash", "tool_input": {"command": 5}},
+    ],
+)
+def test_malformed_or_unrelated_input_fails_open(monkeypatch: pytest.MonkeyPatch, payload: object) -> None:
+    def _boom(command: str, cwd: str | None = None) -> str:
+        raise AssertionError("dispatch must not run")
+
+    monkeypatch.setattr(agent_guard, "dispatch", _boom)
+    _feed(monkeypatch, payload)
+
+    assert agent_guard.junie_main() == agent_guard.ALLOW_EXIT
+
+
+def test_cli_routes_junie_flag(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(agent_guard, "dispatch", lambda command, cwd=None: "no")
+    _feed(monkeypatch, _event("git push"))
+
+    assert agent_guard.cli(["--junie"]) == agent_guard.DENY_EXIT
diff --git a/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py b/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py
index 60b5a8e2..a0c66b81 100644
--- a/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py
+++ b/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py
@@ -208,6 +208,7 @@ HARNESS_VOCAB = {
     "OpenCode",
     "Kiro",
     "Grok",
+    "Junie",
 }
 AGNOSTIC_HARNESS = "agnostic"
 _HARNESS_RE = re.compile(r"^\*\*Harness:\*\*[ \t]+(.+?)[ \t]*$", re.MULTILINE)