@humwick/runner 0.0.0-stage → 0.130.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +46 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse guard (#2834): steers project gate commands run via Bash/Monitor to relay_run.
|
|
3
|
+
# FAIL-OPEN: the decision lives in gate-run-guard.ts (shared SDK gate classifier); every
|
|
4
|
+
# precondition miss here (not relay-managed, read-only, no bun) exits 0 with no output.
|
|
5
|
+
[ -n "${AGENT_RELAY_RUNNER_PORT:-}" ] || exit 0
|
|
6
|
+
[ "${AGENT_RELAY_APPROVAL:-}" != "read-only" ] || exit 0
|
|
7
|
+
command -v bun >/dev/null 2>&1 || exit 0
|
|
8
|
+
HOOK_DIR="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks"
|
|
9
|
+
exec bun "${HOOK_DIR}/gate-run-guard.ts" 2>/dev/null || exit 0
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// PreToolUse guard (#2834): a Bash/Monitor call that runs one of THIS project's own gates
|
|
2
|
+
// (`iterate`, `delivery`, `gates[].command` in the session repo's land-gates.json) is denied with
|
|
3
|
+
// a steer to `relay_run`. FAIL-OPEN: any doubt or error prints nothing and exits 0. Bundled to a
|
|
4
|
+
// dependency-free .mjs for provisioned copies (scripts/build-claude-monitor.ts).
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { classifyGateRun, lexCommandLine, unwrapGroupCommand, unwrapShellCommand } from "@humwick/sdk/gate-run-classify";
|
|
7
|
+
import { findRepoLandGatesConfig, gateCommandsForCwd } from "@humwick/sdk/land-gates";
|
|
8
|
+
import { trustedFlatCommand } from "./shell-trust";
|
|
9
|
+
import { waitLoopDenial } from "./wait-loop-guard";
|
|
10
|
+
|
|
11
|
+
// Past this the command is an embedded script, not a gate launch; skip parsing it.
|
|
12
|
+
const MAX_COMMAND_CHARS = 64 * 1024;
|
|
13
|
+
// Mirrors the classifier's own unwrap depth (sdk MAX_SHELL_UNWRAP_DEPTH).
|
|
14
|
+
const MAX_UNWRAP_DEPTH = 2;
|
|
15
|
+
|
|
16
|
+
const SETUP_COMMANDS = new Set(["cd", "pushd", "export", "set", "source", ".", "echo", "printf", "true"]);
|
|
17
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
18
|
+
const SHELL_BINARIES = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
19
|
+
// Shell flags that prevent execution; everything else (--login, -o, -l, unknown) still runs.
|
|
20
|
+
const SHELL_NON_EXEC_LONG = new Set(["--help", "--version", "--dump-po-strings", "--dump-strings"]);
|
|
21
|
+
// Env flags that prevent execution; everything else still runs.
|
|
22
|
+
const ENV_NON_EXEC_LONG = new Set(["--help", "--version"]);
|
|
23
|
+
const shellQuote = (word: string): string => (/^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`);
|
|
24
|
+
|
|
25
|
+
/** The classifier reads only a gate's first segment, so a `cd x && gate` / `printf ok && gate` config would
|
|
26
|
+
* turn the setup piece into a "gate". Reduce each configured gate to its first real command. */
|
|
27
|
+
function normalizeGate(gate: string | undefined): string | undefined {
|
|
28
|
+
if (!gate) return gate;
|
|
29
|
+
const segments = lexCommandLine(gate).filter((words) => !SETUP_COMMANDS.has(words[0] ?? "") && !words.every((word) => ASSIGNMENT.test(word)));
|
|
30
|
+
return segments[0]?.map(shellQuote).join(" ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const repoRootOf = (dir: string): string | undefined => findRepoLandGatesConfig(dir)?.repoRoot;
|
|
34
|
+
|
|
35
|
+
/** True when a `cd`/`pushd` in the command, or in any shell wrapper layer the classifier peels, leaves the
|
|
36
|
+
* session's project (or cannot be resolved): the gate, if any, runs elsewhere. */
|
|
37
|
+
function leavesProject(command: string, cwd: string): boolean {
|
|
38
|
+
const home = repoRootOf(cwd);
|
|
39
|
+
let candidate: string | undefined = command;
|
|
40
|
+
for (let depth = 0; depth <= MAX_UNWRAP_DEPTH && candidate !== undefined; depth += 1) {
|
|
41
|
+
let dir = cwd;
|
|
42
|
+
for (const words of lexCommandLine(candidate)) {
|
|
43
|
+
if (words[0] !== "cd" && words[0] !== "pushd") continue;
|
|
44
|
+
const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
|
|
45
|
+
if (!target || target === "-" || /^~|\$|`/.test(target)) return true;
|
|
46
|
+
dir = resolve(dir, target);
|
|
47
|
+
if (repoRootOf(dir) !== home) return true;
|
|
48
|
+
}
|
|
49
|
+
candidate = unwrapShellCommand(candidate) ?? unwrapGroupCommand(candidate);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The shared classifier stays shell-grammar-unaware on purpose (heavy-lease callers), so shell edge cases
|
|
55
|
+
// fail open HERE only (shell-trust.ts); checked on the raw text, so wrapper contents are covered too.
|
|
56
|
+
|
|
57
|
+
const baseBinary = (word: string): string => (word.split("/").pop() ?? word).toLowerCase();
|
|
58
|
+
|
|
59
|
+
// True when this layer's shell/env wrapper carries a KNOWN non-executing flag (-n,
|
|
60
|
+
// --help/--version and the like). Inverted direction: unknown flags stay executing, so
|
|
61
|
+
// `bash --login -c '<gate>'` and `bash -o pipefail -c '<gate>'` still deny.
|
|
62
|
+
function wrapperHasNonExecutingOption(candidate: string): boolean {
|
|
63
|
+
const words = lexCommandLine(candidate)[0] ?? [];
|
|
64
|
+
let index = 0;
|
|
65
|
+
while (index < words.length && ASSIGNMENT.test(words[index]!)) index += 1;
|
|
66
|
+
const binary = baseBinary(words[index] ?? "");
|
|
67
|
+
if (SHELL_BINARIES.has(binary)) {
|
|
68
|
+
for (let i = index + 1; i < words.length; i += 1) {
|
|
69
|
+
const flag = words[i]!;
|
|
70
|
+
if (!flag.startsWith("-") || flag === "--") break;
|
|
71
|
+
if (flag.startsWith("--")) {
|
|
72
|
+
if (SHELL_NON_EXEC_LONG.has(flag.split("=", 2)[0]!)) return true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (flag.includes("n")) return true;
|
|
76
|
+
if (/^-[a-z]*c$/.test(flag)) break;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (binary === "env") {
|
|
81
|
+
for (let i = index + 1; i < words.length; i += 1) {
|
|
82
|
+
const flag = words[i]!;
|
|
83
|
+
if (!flag.startsWith("-") || flag === "--") break;
|
|
84
|
+
if (flag.startsWith("--") && ENV_NON_EXEC_LONG.has(flag.split("=", 2)[0]!)) return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// True when a quoted `-c` script is followed by a trailing PLAIN word (`bash -c '…' _ …'):
|
|
91
|
+
// that word becomes `$0` data, not executed. Redirections, pipes, `;` and `&` after the
|
|
92
|
+
// script are outer-command syntax, not operands, so they still deny. Unquoted scripts are
|
|
93
|
+
// left to the classifier (joining them is its documented conservative choice).
|
|
94
|
+
function shellHasExtraOperands(candidate: string): boolean {
|
|
95
|
+
const match = /^\s*(?:\S+\/)?(sh|bash|zsh|dash|ksh)\s+([\s\S]*)$/i.exec(candidate);
|
|
96
|
+
if (!match) return false;
|
|
97
|
+
let rest = match[2]!;
|
|
98
|
+
for (let guard = 0; guard < 16; guard += 1) {
|
|
99
|
+
rest = rest.replace(/^\s+/, "");
|
|
100
|
+
const flag = /^\S+/.exec(rest)?.[0];
|
|
101
|
+
if (!flag || !flag.startsWith("-")) return false;
|
|
102
|
+
rest = rest.slice(flag.length);
|
|
103
|
+
if (/^-[a-z]*c$/.test(flag)) break;
|
|
104
|
+
if (!rest.trim()) return false;
|
|
105
|
+
}
|
|
106
|
+
rest = rest.replace(/^\s+/, "");
|
|
107
|
+
const quote = rest[0];
|
|
108
|
+
if (quote !== "'" && quote !== '"') return false;
|
|
109
|
+
let i = 1;
|
|
110
|
+
while (i < rest.length && rest[i] !== quote) i += 1;
|
|
111
|
+
if (i >= rest.length) return false;
|
|
112
|
+
const after = rest.slice(i + 1).replace(/^\s+/, "");
|
|
113
|
+
if (!after) return false;
|
|
114
|
+
return !/^(?:\d*\s*[><|;&#])/.test(after);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fail open only when the gate sits on the dead side of a literal short-circuit:
|
|
118
|
+
// `false && gate` and `true || gate` never run it. A gate on the live side (`gate ||
|
|
119
|
+
// echo`, `gate && echo done`) still denies, as do `true && gate` and `false || gate`.
|
|
120
|
+
function layerHasShortCircuit(candidate: string): boolean {
|
|
121
|
+
const parts: Array<{ text: string; op: string | null }> = [];
|
|
122
|
+
let current = "";
|
|
123
|
+
let inSingle = false;
|
|
124
|
+
let inDouble = false;
|
|
125
|
+
let pendingOp: string | null = null;
|
|
126
|
+
const push = (): void => {
|
|
127
|
+
parts.push({ text: current, op: pendingOp });
|
|
128
|
+
pendingOp = null;
|
|
129
|
+
current = "";
|
|
130
|
+
};
|
|
131
|
+
for (let i = 0; i < candidate.length; i += 1) {
|
|
132
|
+
const char = candidate[i]!;
|
|
133
|
+
if (inSingle) { current += char; if (char === "'") inSingle = false; continue; }
|
|
134
|
+
if (inDouble) {
|
|
135
|
+
current += char;
|
|
136
|
+
if (char === "\\") { i += 1; if (i < candidate.length) current += candidate[i]; continue; }
|
|
137
|
+
if (char === '"') inDouble = false;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (char === "\\") { current += char; i += 1; if (i < candidate.length) current += candidate[i]; continue; }
|
|
141
|
+
if (char === "'") { inSingle = true; current += char; continue; }
|
|
142
|
+
if (char === '"') { inDouble = true; current += char; continue; }
|
|
143
|
+
if (char === "&" && candidate[i + 1] === "&") { push(); pendingOp = "&&"; i += 1; continue; }
|
|
144
|
+
if (char === "|" && candidate[i + 1] === "|") { push(); pendingOp = "||"; i += 1; continue; }
|
|
145
|
+
current += char;
|
|
146
|
+
}
|
|
147
|
+
push();
|
|
148
|
+
// The command deciding the previous part is its last `;`-segment's binary, past assignments.
|
|
149
|
+
const decidingBinary = (text: string): string | undefined => {
|
|
150
|
+
const words = lexCommandLine(text).at(-1) ?? [];
|
|
151
|
+
return words.find((word) => !ASSIGNMENT.test(word));
|
|
152
|
+
};
|
|
153
|
+
for (let j = 1; j < parts.length; j += 1) {
|
|
154
|
+
const prev = decidingBinary(parts[j - 1]!.text);
|
|
155
|
+
if (parts[j]!.op === "&&" && prev === "false") return true;
|
|
156
|
+
if (parts[j]!.op === "||" && prev === "true") return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Strip `-o/-O <name>` (and `+` variants) pairs from a shell wrapper's flag run. The shared
|
|
162
|
+
// unwrap stops at the operand, so `bash -o pipefail -c '<gate>'` never peels; `-o` only sets
|
|
163
|
+
// shell options, so dropping the pair is faithful. Exact reconstruction when nothing drops.
|
|
164
|
+
function dropShellOptionOperands(command: string): string {
|
|
165
|
+
const match = /^\s*(?:\S+\/)?(?:sh|bash|zsh|dash|ksh)\s+([\s\S]*)$/i.exec(command);
|
|
166
|
+
if (!match) return command;
|
|
167
|
+
const regionStart = command.length - match[1]!.length;
|
|
168
|
+
let rest = match[1]!;
|
|
169
|
+
let kept = "";
|
|
170
|
+
for (let guard = 0; guard < 16; guard += 1) {
|
|
171
|
+
const trimmed = rest.replace(/^\s+/, "");
|
|
172
|
+
const word = /^\S+/.exec(trimmed)?.[0];
|
|
173
|
+
if (!word || word[0] === "'" || word[0] === '"') return command;
|
|
174
|
+
if (/^[-+][oO]$/.test(word)) {
|
|
175
|
+
const operand = /^[^\s&|;#]+/.exec(trimmed.slice(word.length).replace(/^\s+/, ""))?.[0];
|
|
176
|
+
if (!operand || operand[0] === "'" || operand[0] === '"') return command;
|
|
177
|
+
rest = trimmed.slice(word.length).replace(/^\s+/, "").slice(operand.length);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
kept += rest.slice(0, rest.length - trimmed.length + word.length);
|
|
181
|
+
rest = trimmed.slice(word.length);
|
|
182
|
+
if (/^-[a-z]*c$/.test(word)) break;
|
|
183
|
+
if (!word.startsWith("-") && !word.startsWith("+")) break;
|
|
184
|
+
}
|
|
185
|
+
return command.slice(0, regionStart) + kept + rest;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// #2843 fail-opens, hook only: the shared classifier is unchanged (heavy-lease callers keep
|
|
189
|
+
// its conservative join/short-circuit behavior). Each layer is checked, so wrapped forms
|
|
190
|
+
// (`bash -lc 'false && gate'`, `bash -lc 'env --help gate'`) fail open through the wrapper.
|
|
191
|
+
function failOpenForContrivedForm(flat: string): boolean {
|
|
192
|
+
let candidate: string | undefined = flat;
|
|
193
|
+
for (let depth = 0; depth <= MAX_UNWRAP_DEPTH && candidate !== undefined; depth += 1) {
|
|
194
|
+
if (wrapperHasNonExecutingOption(candidate)) return true;
|
|
195
|
+
if (shellHasExtraOperands(candidate)) return true;
|
|
196
|
+
if (layerHasShortCircuit(candidate)) return true;
|
|
197
|
+
candidate = unwrapShellCommand(candidate) ?? unwrapGroupCommand(candidate);
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function gateDenial(command: string, flat: string, cwd: string): string | null {
|
|
203
|
+
const configured = gateCommandsForCwd(cwd);
|
|
204
|
+
const commands = {
|
|
205
|
+
iterate: normalizeGate(configured.iterate),
|
|
206
|
+
delivery: normalizeGate(configured.delivery),
|
|
207
|
+
gates: (configured.gates ?? []).map(normalizeGate).filter((gate): gate is string => Boolean(gate)),
|
|
208
|
+
};
|
|
209
|
+
if (!commands.iterate && !commands.delivery && !commands.gates?.length) return null;
|
|
210
|
+
const normalized = dropShellOptionOperands(flat);
|
|
211
|
+
if (failOpenForContrivedForm(normalized)) return null;
|
|
212
|
+
const category = classifyGateRun(normalized, commands);
|
|
213
|
+
if (!category || category === "heuristic") return null;
|
|
214
|
+
if (leavesProject(normalized, cwd)) return null;
|
|
215
|
+
return `This is a project gate: run it with the relay_run MCP tool (waitMs: 0), then end your turn; Relay wakes you once when it finishes. Command: ${command}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function decide(payload: unknown): string | null {
|
|
219
|
+
const input = payload as { tool_name?: unknown; tool_input?: { command?: unknown; run_in_background?: unknown }; cwd?: unknown } | null;
|
|
220
|
+
if (input?.tool_name !== "Bash" && input?.tool_name !== "Monitor") return null;
|
|
221
|
+
const command = input.tool_input?.command;
|
|
222
|
+
if (typeof command !== "string" || !command || command.length > MAX_COMMAND_CHARS) return null;
|
|
223
|
+
const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
224
|
+
const flat = trustedFlatCommand(command);
|
|
225
|
+
const gate = flat === null ? null : gateDenial(command, flat, cwd);
|
|
226
|
+
// A harness-backgrounded Bash already returns the turn, so only foreground waits are redirected (#2993).
|
|
227
|
+
return gate ?? (input.tool_input?.run_in_background === true ? null : waitLoopDenial(command));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const reason = decide(JSON.parse(await Bun.stdin.text()));
|
|
232
|
+
if (reason) {
|
|
233
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }));
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// fail open
|
|
237
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/user-prompt-submit.sh\"",
|
|
9
|
+
"timeout": 5
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/read-only-bash-guard.sh\"",
|
|
21
|
+
"timeout": 5
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Bash|Monitor",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/gate-run-guard.sh\"",
|
|
31
|
+
"timeout": 5
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"matcher": "AskUserQuestion",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/permission-request.sh\"",
|
|
41
|
+
"timeout": 900
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"PermissionRequest": [
|
|
47
|
+
{
|
|
48
|
+
"hooks": [
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/permission-request.sh\"",
|
|
52
|
+
"timeout": 900
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"Stop": [
|
|
58
|
+
{
|
|
59
|
+
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop.sh\"",
|
|
63
|
+
"timeout": 5
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"StopFailure": [
|
|
69
|
+
{
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop-failure.sh\"",
|
|
74
|
+
"timeout": 5
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"SubagentStart": [
|
|
80
|
+
{
|
|
81
|
+
"hooks": [
|
|
82
|
+
{
|
|
83
|
+
"type": "command",
|
|
84
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-start.sh\"",
|
|
85
|
+
"timeout": 5
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"SubagentStop": [
|
|
91
|
+
{
|
|
92
|
+
"hooks": [
|
|
93
|
+
{
|
|
94
|
+
"type": "command",
|
|
95
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.sh\"",
|
|
96
|
+
"timeout": 5
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"PreCompact": [
|
|
102
|
+
{
|
|
103
|
+
"hooks": [
|
|
104
|
+
{
|
|
105
|
+
"type": "command",
|
|
106
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.sh\"",
|
|
107
|
+
"timeout": 5
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"PostCompact": [
|
|
113
|
+
{
|
|
114
|
+
"hooks": [
|
|
115
|
+
{
|
|
116
|
+
"type": "command",
|
|
117
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/post-compact.sh\"",
|
|
118
|
+
"timeout": 5
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"SessionStart": [
|
|
124
|
+
{
|
|
125
|
+
"hooks": [
|
|
126
|
+
{
|
|
127
|
+
"type": "command",
|
|
128
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\"",
|
|
129
|
+
"timeout": 5
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"SessionEnd": [
|
|
135
|
+
{
|
|
136
|
+
"hooks": [
|
|
137
|
+
{
|
|
138
|
+
"type": "command",
|
|
139
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh\"",
|
|
140
|
+
"timeout": 5
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"PostToolUse": [
|
|
146
|
+
{
|
|
147
|
+
"matcher": "Read|Edit|MultiEdit|Write|NotebookEdit",
|
|
148
|
+
"hooks": [
|
|
149
|
+
{
|
|
150
|
+
"type": "command",
|
|
151
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.sh\"",
|
|
152
|
+
"timeout": 5
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
}
|