Skip to content

[minor] MLAI-1288 - Enforce Cursor skill governance through agent-guard - #81

Merged
shmuelqwak merged 6 commits into
mainfrom
feature/MLAI-1288-cursor-governance-via-agent-guard
Aug 30, 2026
Merged

[minor] MLAI-1288 - Enforce Cursor skill governance through agent-guard#81
shmuelqwak merged 6 commits into
mainfrom
feature/MLAI-1288-cursor-governance-via-agent-guard

Conversation

@shmuelqwak

Copy link
Copy Markdown
Collaborator

Moves Cursor skill governance out of in-plugin Node and into agent-guard, and brings the Cursor wiring in line with the Claude Code plugin — where the behaviour genuinely should match, and deliberately not where Cursor differs.

What the hook does now

npm_config_fetch_retries=0 npm_config_fetch_timeout=10000 \
JF_AGENT_GUARD_ENFORCE_DEADLINE="$(($(date +%s) + 25))" \
npx --yes --prefer-offline --registry "${JFROG_AGENT_GUARD_REPO:-…}" \
  @jfrog/agent-guard --enforce-skill --client cursor

Byte-identical on both governed surfaces — beforeSubmitPrompt (unmatched, so every submission) and preToolUse (matcher Read, since Cursor has no Skill tool).

before after
|| exit 2 present removed
--waiver-helper passed removed — gone from agent-guard's flag table
version ${JFROG_AGENT_GUARD_VERSION:+@…} unpinned → always latest
deadline none absolute, computed fresh, no inheritable fallback
fetch bounds none retries=0, timeout=10000
cache none --prefer-offline, refreshed by a sessionStart pre-warm
timeout / failClosed 20 / true 30 / false

Infrastructure failure now fails OPEN

|| exit 2 is gone and failClosed is false. npx missing, a failed install, an unreachable registry, or no configured JFrog server all let the skill through. A machine that cannot get a verdict is not governed by it, and refusing every skill there enforces nothing except the user's inability to work.

A real policy denial still blocks: on Cursor the verdict travels as JSON on stdout with exit 0, so the payload carries it regardless of failClosed. failClosed was only ever a crash net, never how a block is signalled.

This is where Cursor deliberately diverges from Claude Code. The Claude plugin blocks when the guard reaches the check but cannot finish it in time; here that case allows, because a killed or failed hook is not a block under failClosed: false. The README states the divergence rather than papering over it.

Verified against a genuinely unconfigured machine (0 jf servers, no setup.json, no env):

beforeSubmitPrompt -> {"continue":true}   exit=0
preToolUse         -> {}                  exit=0
agent-guard: could not resolve JFrog credentials, so this skill is NOT governed

Both are the correct Cursor allow shapes — explicit JSON, never silence.

The waiver helper is deleted

agent-guard v1.11.0 files waivers itself via --request-waiver, and --waiver-helper is no longer in its flag table. plugins/jfrog/scripts/governance/ is removed entirely, so the plugin holds no credentials and no waiver logic.

Two earlier commits on this branch had rewritten request-waiver.mjs to the Unified Policy Phase II payload. That work is obsolete under the new split, so the branch was recommitted without it.

Nothing may pin the version

All three invocations name @jfrog/agent-guard bare. Proven empirically, not just asserted — with JFROG_AGENT_GUARD_VERSION=9.9.9 exported, argv still carries bare @jfrog/agent-guard. A shipped GA fix therefore reaches users without a plugin release.

Minimum required: agent-guard ≥ 1.11.0. This matters more than it looks. On 1.10.0 the same event blocks:

no project, no credentials
v1.10.0 {"continue":false} — "no project is set"
v1.11.0 {"continue":true} — allowed

1.10.0 decided "no project" locally; 1.11.0 refers it to the service and fails open on unresolvable credentials.

Why sessionStart pre-warms the cache

--prefer-offline on the governed hooks is only safe if something refreshes the npx cache. Without it, a machine can serve a stale agent-guard indefinitely — this was hit in real testing: a cached 1.10.0 kept being used while 1.11.0 was latest, silently reinstating the bug above. It read as a governance verdict, not as a stale binary.

Cursor's hook schema has no async field, so the pre-warm detaches itself:

( npx --yes --registry "${JFROG_AGENT_GUARD_REPO:-…}" @jfrog/agent-guard --version >/dev/null 2>&1 & ) ; exit 0

Measured: returns in 0s, and the detached fetch lands the latest version in the cache. It deliberately omits --prefer-offline, since hitting the registry is its only purpose, and the explicit exit 0 keeps a warm failure from ever failing session start.

Validation

scripts/validate-skill-governance.mjs was rewritten — its old assertions demanded the exact things this PR removes (must end in "|| exit 2", must pass the waiver helper, failClosed === true). 34 checks, all passing, asserting the inverted contract and executing the real command string from hooks.json against a stub npx.

Behavioural checks run against both surfaces with surface-shaped payloads, rather than testing preToolUse and inheriting the result. Notable checks:

  • npm_config_fetch_timeout is asserted by value (<= 10000), so an edit back to npm's 300000 default fails rather than passing a presence check.
  • The deadline check executes the command and reads what the child receives, including that a stale inherited JF_AGENT_GUARD_ENFORCE_DEADLINE does not survive.
  • The pre-warm must exist, must not use --prefer-offline, must not pin a version, must detach, and must end in exit 0.
  • The package-resolution sessionStart hook is located by content, never by index — that list is shared and other features append to it; it has already moved once.

Not verified

  • Windows. Cursor's hook schema has no shell field, so the command runs in the platform's default shell. It is POSIX (${VAR:-default}, $(( ))), so on cmd/PowerShell it cannot run and governed actions are allowed unchecked. Stated in the README; unchanged from before this PR, which was equally POSIX-only.
  • Warm per-call cost. The hook spawns a shell, npx and the guard on every prompt submission and every Read, with no throttle and no verdict cache. --prefer-offline is what makes that affordable, but the warm cost has not been measured.
  • Cold start allows. With no pre-warm completed and failClosed: false, the first governed action on a cold machine can be allowed. The detached pre-warm narrows the window but does not close it.

Release

[minor]0.5.180.6.0, bumped in both plugins/jfrog/.cursor-plugin/plugin.json and .cursor-plugin/marketplace.json as validate-version.yml requires.

shmuelqwak and others added 3 commits August 27, 2026 14:54
Replaces the in-plugin Node governance with a direct npx call to
agent-guard, and brings the Cursor wiring in line with the Claude Code
plugin.

Infrastructure failure now fails OPEN. `|| exit 2` is gone and
`failClosed` is false: npx missing, a failed install, an unreachable
registry or no configured server let the skill through. A machine that
cannot run the guard is not governed by it, and refusing every skill
there enforces nothing except the user's inability to work. A real
policy denial still blocks — on Cursor the verdict travels as JSON with
exit 0, so the payload carries it regardless of failClosed.

The waiver helper is deleted. agent-guard v1.11.0 files waivers itself
via --request-waiver, and --waiver-helper is no longer in its flag
table, so the plugin holds no credentials and no waiver logic.

Nothing may pin the version: the hook resolves `latest` so a shipped GA
fix reaches users without a plugin release. The call also bounds its
fetch, prefers the cache, and passes an absolute deadline computed
fresh at invocation — with no inheritable fallback, since an absolute
instant inherited from an earlier process pins every later invocation
to the past.

No sessionStart pre-warm: Cursor's hook schema has no async field, so
one would stall session start. Cold start therefore allows; tracked as
a follow-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sor-governance-via-agent-guard

# Conflicts:
#	.cursor-plugin/marketplace.json
#	README.md
#	plugins/jfrog/.cursor-plugin/plugin.json
--prefer-offline on the governed hooks is only safe if something refreshes
the npx cache. Claude Code's plugin does that with an async SessionStart
pre-warm; dropping it here left a machine able to serve a stale binary
indefinitely.

Measured: a cached 1.10.0 kept being used while 1.11.0 was latest, which
silently reinstated a bug 1.11.0 had fixed — 1.10.0 decides "no project"
locally and BLOCKS, where 1.11.0 refers it to the service and fails open
on unresolvable credentials. The stale cache read as a governance verdict.

Cursor's hook schema has no async field, so the command detaches itself:
a subshell background plus an explicit exit 0, which returns in 0s and
never fails session start. It deliberately omits --prefer-offline, since
hitting the registry is its only purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shmuelqwak
shmuelqwak requested a review from a team as a code owner August 27, 2026 15:01
An absent date(1) made $(($(date +%s) + 25)) expand to 25 — an epoch in
1970 — which floors the enforcement budget at 500ms and blocks every
skill, reported as a governance timeout. Passing empty instead is safe:
agent-guard ignores an empty deadline and uses its own default budget.

failClosed: false does not cover this. The block arrives as a deny in
the JSON payload, which agent-guard produced deliberately; failClosed
only governs a hook that fails on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@YoniMelki YoniMelki left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comments list the required changes.

Comment thread README.md Outdated
Comment thread .github/workflows/validate-skill-governance.yml
Comment thread README.md Outdated
Comment thread plugins/jfrog/hooks/hooks.json
Comment thread scripts/validate-skill-governance.mjs
Comment thread .github/workflows/validate-skill-governance.yml
Comment thread plugins/jfrog/hooks/hooks.json Outdated
shmuelqwak and others added 2 commits August 28, 2026 14:38
… validator

The README claimed the hook always exits 0 and that running out of time
allows. Both are wrong: when the Agent Guard's own budget expires it
writes a refusal and exits 2, so it BLOCKS. What allows is Cursor killing
the hook at its own timeout — a different clock, and the longer of the
two. The three cases are now stated separately, and a paragraph that had
been duplicated verbatim is gone.

The capabilities table promised more coverage than the Skills governance
section admits; it now names the same two entry points and the same gap.

validate-skill-governance.yml is pull-request only, so nothing checked
the merge commit a release is cut from. release.yml now runs the script
directly, matching what it already does for validate-template.mjs and
what CONTRIBUTING.md requires.

Adds validator cases for output that is not valid JSON and for empty
stdout with exit 0, and watches modules/** so re-added governance code
cannot slip past the trigger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sionStart

The sessionStart pre-warm does not work. Measured under a real Cursor
session, twice: Cursor kills the hook's process group, and nothing
outruns it — not a detached subshell, not sh -mc, not a perl setsid
escape. It was dead code that implied a freshness guarantee.

That left --prefer-offline with nothing refreshing it, which freezes
agent-guard at whatever version was first fetched. That is the failure
that made a cached 1.10.0 keep serving verdicts a released 1.11.0 had
already fixed.

The refresh now happens on a hook Cursor actually waits for.
beforeSubmitPrompt drops --prefer-offline and revalidates against the
registry; preToolUse keeps it and reads what that left behind. Measured
warm: 1091ms revalidating, 324ms from cache. A prompt fires once per
turn, a Read dozens of times, so the cost lands where it is invisible
and the hot path stays fast. Session start pays nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shmuelqwak
shmuelqwak merged commit 4a92bc3 into main Aug 30, 2026
4 checks passed
@shmuelqwak
shmuelqwak deleted the feature/MLAI-1288-cursor-governance-via-agent-guard branch August 30, 2026 05:18
AsalaZr added a commit that referenced this pull request Aug 30, 2026
Resolve version conflict: bump 0.6.1 (patch on top of 0.6.0 from #81)

Co-authored-by: Cursor <cursoragent@cursor.com>
shmuelqwak added a commit that referenced this pull request Sep 2, 2026
…ers the payload

Skill governance has never functioned on Cursor in any published version. It entered
main already broken, in 4a92bc3 (#81, 2026-08-30): hooks.json carried no governance
hooks before that commit, and every commit since has had the defect. Both governed
surfaces allowed every skill without ever contacting the governance service, silently
and at exit 0.

On macOS and Linux Cursor does not write the event to the hook process's stdin. It
base64s the JSON into the command string and pipes it in from a pipeline the spawned
shell builds itself, while closing the child's own stdin:

  workbench.desktop.main.js   R = `printf %s '${b64}' | base64 -d | ${command}`
  extensionHostProcess.js     stdio: [ pipeStdin ? "pipe" : "ignore", "pipe", "pipe" ]

Our command began `_JFAG_NOW=$(date +%s 2>/dev/null); …`, and that top-level `;`
terminates Cursor's pipeline: base64 -d piped into a bare assignment that reads
nothing, and npx ran as a separate command inheriting the shell's stdin — /dev/null.
agent-guard read 0 bytes, could not classify the event, and rendered its no-opinion
allow, which is indistinguishable from "this prompt was not a skill invocation".

Computing the deadline inside a command substitution scopes the `;` and keeps the hook
one simple command, so it stays the tail of Cursor's pipeline. The intent of the
defensive clock read is preserved, not reverted: verified under /bin/sh, /bin/zsh and
/bin/bash that a governed skill blocks and that the deadline still degrades to EMPTY
when date(1) cannot be read.

The validator could not catch this because it delivered the payload the way Claude Code
does — on the shell's stdin — so all 34 checks passed against a hook that delivered
nothing. runHook now reproduces Cursor's wrapper exactly, with fd 0 as /dev/null, which
makes the existing stdin assertions load-bearing. Two checks are added: a static one
rejecting any top-level `;`, `&&` or `||` in a governed command, and a behavioural one
asserting the payload survives under every shell Cursor might pick. Against the previous
hooks.json the suite now fails 8 ways.

Evidence from every Cursor hook log on the reporting machine: 384 runs with the `;`
produced 0 blocks and 0 agent-guard stderr; 145 runs without it produced 8 blocks, and
those all came from a local dev install predating the merge. Re-confirmed against the
published plugin restored byte-for-byte: 8 governed runs, 0 stderr, and 0 requests
reaching governance-manager-webapp.
shmuelqwak added a commit that referenced this pull request Sep 2, 2026
…ers the payload

Skill governance has never functioned on Cursor in any published version. It entered
main already broken, in 4a92bc3 (#81): hooks.json carried no governance hooks before
that commit, and every commit since has had the defect. Both governed surfaces allowed
every skill without ever contacting the governance service, silently and at exit 0.

On macOS and Linux Cursor does not write the event to the hook process's stdin. It
base64s the JSON into the command string and pipes it in from a pipeline the spawned
shell builds itself, while closing the child's own stdin:

  workbench.desktop.main.js   R = `printf %s '${b64}' | base64 -d | ${command}`
  extensionHostProcess.js     stdio: [ pipeStdin ? "pipe" : "ignore", "pipe", "pipe" ]

Our command began `_JFAG_NOW=$(date +%s 2>/dev/null); …`, and that top-level `;`
terminates Cursor's pipeline: base64 -d piped into a bare assignment that reads
nothing, and npx ran as a separate command inheriting the shell's stdin — /dev/null.
agent-guard read 0 bytes, could not classify the event, and rendered its no-opinion
allow, which is indistinguishable from "this prompt was not a skill invocation".

Computing the deadline inside a command substitution scopes the `;` and keeps the hook
one simple command, so it stays the tail of Cursor's pipeline. The intent of the
defensive clock read is preserved, not reverted: verified under /bin/sh, /bin/zsh and
/bin/bash that a governed skill blocks and that the deadline still degrades to EMPTY
when date(1) cannot be read.

The validator could not catch this because it delivered the payload the way Claude Code
does — on the shell's stdin — so all 34 checks passed against a hook that delivered
nothing. runHook now reproduces Cursor's wrapper exactly, with fd 0 as /dev/null, which
makes the existing stdin assertions load-bearing. Two checks are added: a static one
rejecting any top-level `;`, `&&` or `||` in a governed command, and a behavioural one
asserting the payload survives under every shell Cursor might pick. Against the previous
hooks.json the suite now fails 8 ways.

Verified against the published plugin restored byte-for-byte, swapping only these two
command lines: before the fix, governed runs reached the governance service zero times;
after it, a request arrives within the hook's own window.
shmuelqwak added a commit that referenced this pull request Sep 3, 2026
…de path

Addresses review feedback.

The manifests stay at 0.6.3, which is already tagged, so a [patch] merge would fail
the release job's existing-tag guard before publishing anything. Both carriers move
to 0.6.4 together; they were already in sync, so this is one bump, not a mismatch.

The sandbox comment claimed a PATH without date(1) "would silently yield $(( + 25))
= 25 - an epoch in 1970". That describes the pre-c4cb1d1 inline form. The command
this repo has shipped since #81 carries the ${_JFAG_NOW:+…} guard and degrades to an
EMPTY deadline instead, which agent-guard ignores in favour of its own budget. The
comment has been wrong since it was written; it sits in the block this change edits.

The degrade itself was asserted only in text: every behavioural run kept date(1) on
PATH, isolate mode included, so the guard was never executed. That is the same class
of gap this PR exists to close - the suite asserted the payload was forwarded while
the hook forwarded nothing - so the last text-only assertion is closed here. A second
sandbox PATH without date(1) drives the real command and asserts the deadline is
EMPTY and the payload still arrives.

The check is load-bearing, not decorative: swapping the command back to the inline
$(($(date +%s) + 25)) form fails it on both surfaces, because that form yields 25
rather than empty, which would floor agent-guard's budget at 500ms and block every
skill.

41 checks pass; validate-template and the 23 unit tests are unaffected.
shmuelqwak added a commit that referenced this pull request Sep 3, 2026
…or delivers the payload (#86)

Fixes [MLAI-1310](https://jfrog-int.atlassian.net/browse/MLAI-1310).

**Cursor skill governance has never functioned in any published plugin
version.** Both governed surfaces — `beforeSubmitPrompt` and
`preToolUse` — allowed every skill without ever contacting the
governance service, silently, at exit 0.

## What was wrong

On macOS and Linux, Cursor does **not** write the event to the hook
process's stdin. It base64s the JSON into the command string, pipes it
in from a pipeline the spawned shell builds itself, and closes the
child's own stdin:

```js
// workbench.desktop.main.js
E === 1 ? (L = JSON.stringify(s), R = S)                       // Windows: real stdin
        : (L = undefined, R = `printf %s '${I}' | base64 -d | ${S}`)

// extensionHostProcess.js
const m = i?.pipeStdin ?? false;                               // hooks never pass pipeStdin
pc(process.env.SHELL || "/bin/sh", ["-c", h],
   { stdio: [ m ? "pipe" : "ignore", "pipe", "pipe" ] }, …)    // fd 0 = /dev/null
```

`S` is our `command`, concatenated raw — so **the hook command is the
tail of a pipeline.** Ours began `_JFAG_NOW=$(date +%s 2>/dev/null); …`,
and after concatenation the shell sees:

```sh
printf %s '<b64>' | base64 -d | _JFAG_NOW=$(date +%s 2>/dev/null); npm_config_… npx … --enforce-skill
```

That top-level `;` **terminates the pipeline**. `base64 -d` pipes into a
bare assignment that reads nothing, and `npx` runs as a *separate*
command inheriting the shell's stdin — `/dev/null`. agent-guard reads 0
bytes, cannot classify the event, and renders its no-opinion allow,
which is byte-identical to "this prompt was not a skill invocation".

## When it entered

| commit | `enforce-skill` | top-level `;` | |
|---|---|---|---|
| `8e79b6e` | 2 | **0** | governance introduced — worked |
| `39bfd5f` | 2 | **0** | worked |
| **`c4cb1d1`** | 2 | **2** | **← authored the defect** ("Degrade to no
deadline when the clock cannot be read") |
| `1ba4b97` | 2 | 2 | |
| **`4a92bc3`** | 2 | 2 | **← shipped it** (squash-merge of #81) |

`hooks.json` on `main` had no governance hooks before `4a92bc3`
(`enforce-skill` occurrences 0 → 2 there), and the squash collapsed the
branch — so `8e79b6e`'s working form **never reached `main`**. `c4cb1d1`
is not an ancestor of `main`. This is not a regression from a working
release; the feature shipped dead.

The only blocks anywhere in the Cursor hook logs came from a local dev
install — visible as `Running script in directory:
…/plugins/local/jfrog` — on the pre-`c4cb1d1` branch state.

## The fix

Compute the deadline inside a command substitution, so the `;` it needs
is scoped and the hook stays **one simple command**:

```sh
JF_AGENT_GUARD_ENFORCE_DEADLINE="$(_JFAG_NOW=$(date +%s 2>/dev/null); echo ${_JFAG_NOW:+$((_JFAG_NOW + 25))})" npx --yes …
```

The intent of the defensive clock read is **preserved, not reverted**:
verified under `/bin/sh`, `/bin/zsh` and `/bin/bash` that a governed
skill blocks, and that the deadline still degrades to **empty** when
`date(1)` cannot be read (agent-guard ignores an empty deadline; a
garbage epoch would floor its budget at 500ms and block everything).

No wrapper script, no new file in the enforcement path.

## Evidence — the semicolon isolated

All four runs use Cursor's exact wrapper, a real `$SHELL`, and fd 0 =
`/dev/null`:

| hook command | result |
|---|---|
| `_JFAG_NOW=$(date +%s); … agent-guard` (shipped) | `{"continue":true}`
— governance never ran |
| inline deadline, no `;` | `{"continue":false, …blocking…}` |
| bare `agent-guard`, no assignments | `{"continue":false, …blocking…}`
|
| `true; agent-guard` | `{"continue":true}` |

A no-op `true;` is sufficient to break it, which rules out the deadline,
`date`, and the environment.

## Verified against the real published plugin

Install restored byte-for-byte to the published commit, skills invoked
in Cursor, then only these two command lines swapped for the fixed form
and Cursor restarted:

| | hook duration | governance service |
|---|---|---|
| **before** (`;`) | ~1450ms | **no request** across 8 governed runs |
| **after** (fixed) | **5400ms** | **request received**, inside the
hook's own window |

Before the fix, a skill known to be policy-blocked was allowed — the
same skill that blocked from the local dev install on the pre-`c4cb1d1`
state.

**Triage note:** duration and stderr are both weak signals on their own.
A dead hook still costs ~1.2–1.5s because the prompt hook revalidates
npx regardless, and agent-guard only emits `diag()` when something
*fails* — a clean successful allow is silent. The reliable tell is
whether a request reaches the service.

## Why CI missed it, and what now catches it

`validate-skill-governance.mjs` executed the real command string, but
delivered the payload the way **Claude Code** does — on the shell's
stdin:

```js
spawnSync(SH, ["-c", command], { input: Buffer.from(payload), … })
```

It never reproduced Cursor's concatenation, so all 34 checks — including
*"forwards stdin verbatim"* — passed against a hook that delivered
nothing.

`runHook` now reproduces Cursor's wrapper exactly, with `fd 0` as
`/dev/null`. That single change makes every existing stdin assertion
load-bearing. Two checks are added:

- a **static** one rejecting any top-level `;`, `&&` or `||` in a
governed command, which fails with the offending text rather than a
mystery allow;
- a **behavioural** one asserting the payload survives under every shell
Cursor might pick (`$SHELL || /bin/sh`), skipping shells absent from the
runner.

`base64` is symlinked into the sandbox PATH for the same reason `date`
already was — Cursor's delivery needs it, and without it every stdin
assertion would fail for an unrelated reason.

**Against the previous `hooks.json` the suite now fails 8 ways**,
including both behavioural stdin checks:

```
FAIL beforeSubmitPrompt computes the deadline fresh, with no inheritable fallback
FAIL preToolUse computes the deadline fresh, with no inheritable fallback
FAIL no governed command has a top-level ';', '&&' or '||' (it is the tail of Cursor's pipeline)
FAIL the payload survives Cursor's pipeline under every shell Cursor may pick
FAIL beforeSubmitPrompt: forwards stdin verbatim and hands agent-guard the expected argv
FAIL beforeSubmitPrompt: hands agent-guard a deadline in the future, computed at invocation
FAIL preToolUse: forwards stdin verbatim and hands agent-guard the expected argv
FAIL preToolUse: hands agent-guard a deadline in the future, computed at invocation
```

On this branch: **all checks pass**, plus `validate-template` and 23
unit tests.

The general rule worth keeping: **a hook validator must invoke the
command exactly as the target client delivers it, per client.**

## Not the cause

Two reports attributed this to a stdin-reading line in `~/.zshrc`
draining the hook pipe. That is not what happens: Cursor invokes `$SHELL
-c`, so a non-interactive zsh sources `~/.zshenv`, never `~/.zshrc` —
and it is moot anyway, because fd 0 is `/dev/null`, so there is nothing
for an rc file to steal. Those reports reproduced the failure by piping
the payload into the shell's own stdin, a coupling Cursor never creates.
The bash-wrapper workaround they proposed does work, but because `bash
./x.sh` is a *single command*, not because bash skips `~/.zshrc`.

## Scope

Cursor only. `claude-plugin` never had the `;` (still the inline form)
and delivers on real stdin. `vscode-plugin` has the `;` but delivers on
real stdin too (`stdin.write(JSON.stringify(…))`, `stdio:["pipe"…]`), so
it works — it is hardened to the same shape in its pending PR, since
these strings are kept identical across the three plugins and a copy
from there to here would reintroduce this.

## Follow-up (not this PR)

agent-guard renders an empty or unparseable payload as a clean allow
with **no diagnostic at all**, which is what kept this invisible. Worth
a `diag()` on zero bytes plus an opt-in
`JF_AGENT_GUARD_ENFORCE_FAIL_CLOSED`; the default should stay allow,
matching the deliberate fail-open posture.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants