You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#81 added a stderr mirror for policy rejections so callers that only read stdout/stderr can see why a run was refused. The mirror works in compatibility mode, but sshx run --json — the canonical agent contract — only prints the generic line: the block reason (which command, which rule, what to do instead) never reaches stderr, and phase is reported as resolve instead of the admission predicate #81 documented.
Repro (built from origin/main @ 1be5f1d, v0.19.0+1)
Compatibility mode — correct:
$ sshx -h=prod-web --json "docker exec db psql -U app -c 'SELECT 1'"2>&1>/dev/nullsshx: blocked by safety policy (phase=admission, error_kind=blocked, executed=false, exit_code=-1); no remote command ransshx: block reason: ⚠️ Dangerous command blocked | Command: docker exec db psql … | Reason: Direct PostgreSQL client execution ("psql") bypasses the guarded SQL pipeline. Use: sshx sql -h=<host> … | If you are sure, use --force or -f flag
run --json — reason missing, phase different:
$ sshx run --target=prod-web --json -- "docker exec db psql -U app -c 'SELECT 1'"2>&1>/dev/nullsshx: blocked by safety policy (phase=resolve, error_kind=blocked, executed=false, exit_code=-1); no remote command ran
# no "sshx: block reason:" line
The stdout document does contain the reason (error.message), so nothing is lost for a full-document reader — but the stderr mirror, which exists precisely for callers that surface only stdout/stderr, is empty in the mode the docs recommend.
Root cause
Two independent defects in the same path:
reportPolicyRejection (internal/app/lifecycle.go) reads document["error"] as a JSON string. In compat mode the envelope's error is a string, so documentString() works. In run --json the envelope's error is an object {"kind":"blocked","message":"…"}; json.Unmarshal into string fails silently, documentString returns "", and flattenPolicyReason is never called.
run reports blocked plan validation as PhaseResolve, compat reports PhaseAdmission.reportRunRequestFailure hardcodes Phase: execution.PhaseResolve (internal/app/run.go), while the executor sets PhaseAdmission for the identical SafetyCheck failure. fix: policy-block visibility, sshx text read pipelining, and the non-leading sudo boundary #81's own doc comment defines the predicate as phase=admission, so a caller that filters on it silently misses every run --json block.
Expected
run --json emits the same sshx: block reason: … stderr line as compat mode (read error.message when error is an object).
One phase for one decision: either both paths report admission, or the documented predicate covers both values.
A regression test that runs the block through both entry points and asserts stderr content + phase parity (lifecycle_policy_test.go currently only asserts the generic line for one shape).
Environment
sshx: built from origin/main1be5f1d (v0.19.0-1-g1be5f1d); reproduced on installed v0.17.0 as well.
Observed while triaging agent-facing diagnostics (RAM-TA3 Tools/sshx-experience-log.md, entry 2026-09-27).
Summary
#81 added a stderr mirror for policy rejections so callers that only read
stdout/stderrcan see why a run was refused. The mirror works in compatibility mode, butsshx run --json— the canonical agent contract — only prints the generic line: the block reason (which command, which rule, what to do instead) never reaches stderr, andphaseis reported asresolveinstead of theadmissionpredicate #81 documented.Repro (built from
origin/main@1be5f1d, v0.19.0+1)Compatibility mode — correct:
run --json— reason missing, phase different:The stdout document does contain the reason (
error.message), so nothing is lost for a full-document reader — but the stderr mirror, which exists precisely for callers that surface only stdout/stderr, is empty in the mode the docs recommend.Root cause
Two independent defects in the same path:
reportPolicyRejection(internal/app/lifecycle.go) readsdocument["error"]as a JSON string. In compat mode the envelope'serroris a string, sodocumentString()works. Inrun --jsonthe envelope'serroris an object{"kind":"blocked","message":"…"};json.Unmarshalintostringfails silently,documentStringreturns"", andflattenPolicyReasonis never called.runreports blocked plan validation asPhaseResolve, compat reportsPhaseAdmission.reportRunRequestFailurehardcodesPhase: execution.PhaseResolve(internal/app/run.go), while the executor setsPhaseAdmissionfor the identicalSafetyCheckfailure. fix: policy-block visibility, sshx text read pipelining, and the non-leading sudo boundary #81's own doc comment defines the predicate asphase=admission, so a caller that filters on it silently misses everyrun --jsonblock.Expected
run --jsonemits the samesshx: block reason: …stderr line as compat mode (readerror.messagewhenerroris an object).admission, or the documented predicate covers both values.lifecycle_policy_test.gocurrently only asserts the generic line for one shape).Environment
origin/main1be5f1d(v0.19.0-1-g1be5f1d); reproduced on installed v0.17.0 as well.Tools/sshx-experience-log.md, entry 2026-09-27).