Skip to content

feat: add openclaw tunnel command for managed SSH port-forwarding#46653

Closed
mager wants to merge 1 commit into
openclaw:mainfrom
mager:feature/tunnel-command
Closed

feat: add openclaw tunnel command for managed SSH port-forwarding#46653
mager wants to merge 1 commit into
openclaw:mainfrom
mager:feature/tunnel-command

Conversation

@mager

@mager mager commented Mar 14, 2026

Copy link
Copy Markdown

Summary

  • Problem: Users managing a remote OpenClaw gateway over SSH must maintain manual shell aliases (autossh -M 0 -f -N host, pkill -f autossh, lsof -i | grep 18789) with no state tracking or discoverability.
  • Why it matters: SSH port-forwarding is the primary connectivity method for headless/VPS gateway setups. It's undiscoverable (--help doesn't mention it) and un-trackable (no PID management, stale processes are common).
  • What changed: Added openclaw tunnel up/down/status subcommand. Adds src/cli/tunnel-cli.ts, registers it in register.subclis.ts, adds docs/cli/tunnel.md, and wires it into docs.json nav.
  • What did NOT change: No existing commands, SSH infra, or gateway behavior modified. Purely additive.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #

User-visible / Behavior Changes

  • New top-level command openclaw tunnel with three subcommands: up, down, status
  • tunnel up writes ~/.openclaw/tunnel.pid.json to track tunnel state across shells
  • tunnel status auto-cleans stale PID files if the SSH process has exited

Security Impact (required)

  • New permissions/capabilities? No — spawns /usr/bin/ssh exactly as users already do manually
  • Secrets/tokens handling changed? No
  • New/changed network calls? No — SSH is user-initiated, same as ssh from the terminal
  • Command/tool execution surface changed? Yes — spawns /usr/bin/ssh as a detached child process
  • Data access scope changed? No

The SSH target, identity file path, and port are provided by the user at invocation time. The command uses the same BatchMode=yes + StrictHostKeyChecking=yes flags as the existing startSshPortForward() in src/infra/ssh-tunnel.ts, preventing any interactive credential prompts. The spawned process is unprivileged. No new attack surface beyond what a user already has with ssh in their shell.

Repro + Verification

Environment

  • OS: macOS 15.3 (Darwin arm64)
  • Runtime: Node v25.6.1
  • Model/provider: N/A (CLI only)
  • Integration/channel: N/A
  • Relevant config: gateway.bind: loopback (default)

Steps

  1. Ensure SSH key auth is configured to a remote host
  2. openclaw tunnel up user@remote-host
  3. openclaw tunnel status
  4. openclaw tunnel down
  5. openclaw tunnel status (should show no tunnel running)

Expected

  • Step 2: tunnel starts, PID file written, success message with port
  • Step 3: shows target, port, PID, active status
  • Step 4: tunnel killed, PID file deleted, confirmation message
  • Step 5: "No tunnel is running."

Actual

  • All steps behave as expected on macOS arm64 with a local SSH target

Evidence

  • TypeScript compiles clean (pnpm exec tsc --noEmit, zero errors)
  • Failing test/log before + passing after (no existing tunnel tests to update; unit tests for parseSshTarget in ssh-tunnel.test.ts are unaffected)

Human Verification (required)

  • Verified scenarios: tunnel up with valid SSH target, port binding detection, PID file creation; tunnel down with running and already-stopped tunnel; tunnel status with active, stale, and absent PID file; TypeScript type-checks clean
  • Edge cases checked: Stale PID file from dead process (auto-cleaned in both down and status); duplicate tunnel up when already running (errors with clear message); invalid SSH target format (errors with format hint)
  • What I did not verify: Windows/WSL behavior; non-default OPENCLAW_STATE_DIR override; SSH host key verification failure path (relies on BatchMode=yes exiting cleanly, which it does)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — purely additive, no existing behavior changed
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert quickly: remove the tunnel entry from register.subclis.ts entries array
  • Files to restore: src/cli/tunnel-cli.ts (delete), src/cli/program/register.subclis.ts (revert 9-line addition), docs/cli/tunnel.md (delete), docs/docs.json (revert 1-line addition)
  • Known bad symptoms: PID file at ~/.openclaw/tunnel.pid.json from a stale tunnel — run openclaw tunnel down or rm ~/.openclaw/tunnel.pid.json

Risks and Mitigations

  • Risk: Detached SSH process outlives the CLI and is not killed if the user never runs tunnel down
    • Mitigation: This is intentional and matches the behavior of autossh -f. Users can always run tunnel down or tunnel status to inspect and kill. The PID file provides the handle.
  • Risk: Port collision if another process is already bound to the gateway port
    • Mitigation: isPortBound() check during startup detects this and the tunnel's ExitOnForwardFailure=yes will cause SSH to exit, which the wait-loop catches and surfaces as a clear error.

Adds three subcommands to manage persistent SSH tunnels to a remote gateway:

- `openclaw tunnel up <target>` — starts a detached SSH port-forward,
  waits for the port to bind, and writes state to ~/.openclaw/tunnel.pid.json
- `openclaw tunnel down` — stops the tunnel (SIGTERM → SIGKILL) and
  removes the PID file
- `openclaw tunnel status` — shows target, PID, port, and liveness;
  auto-cleans stale PID files

Reuses parseSshTarget() from src/infra/ssh-tunnel.ts (no logic duplication).
Spawns native /usr/bin/ssh with detached: true so the tunnel outlives the CLI
process. Uses the same keepalive flags (ServerAliveInterval, ExitOnForwardFailure,
BatchMode) as the existing SSH infra.

Motivation: users currently manage this via shell aliases
(`autossh -M 0 -f -N host`, `pkill -f autossh`, `lsof -i | grep 18789`).
This makes it a first-class, trackable CLI command — no autossh dependency.

Also adds docs/cli/tunnel.md and wires the command into docs.json nav.
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation cli CLI command changes size: M labels Mar 14, 2026
@greptile-apps

greptile-apps Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds openclaw tunnel up/down/status subcommands that manage a detached SSH port-forward process, writing state to ~/.openclaw/tunnel.pid.json. The implementation is generally well-structured — it reuses parseSshTarget(), applies the same keepalive flags as the existing SSH infra, handles stale PID cleanup, and uses SIGTERM → SIGKILL escalation. Two logic bugs need addressing before merge:

  • STATE_DIR not guaranteed to exist (writeTunnelState): on a fresh install ~/.openclaw may not exist yet, causing fs.writeFileSync to throw ENOENT.
  • False-positive success race in tunnel up: if the target port is already occupied when SSH spawns, ExitOnForwardFailure=yes will cause SSH to exit quickly, but there is a narrow window (0–150 ms) where both isPidAlive and isPortBound return true for the wrong reasons — resulting in a stale PID being written and the command incorrectly reporting success.

Additionally, isPortBound silently treats any non-EADDRINUSE OS error as "port free", which would produce a confusing 8-second timeout rather than a clear error. The existing canConnectLocal() helper in src/infra/ssh-tunnel.ts is a more reliable alternative for polling readiness.

Confidence Score: 3/5

  • Not safe to merge as-is — two logic bugs can cause ENOENT crashes on fresh installs and silent false-positive success when the port is pre-occupied.
  • The core approach is sound and the overall structure is good, but two concrete logic bugs (missing mkdirSync before the PID write, and the isPortBound race condition) can cause incorrect behavior in real usage scenarios. Neither is catastrophic but both are deterministically reproducible given specific conditions.
  • src/cli/tunnel-cli.ts — specifically writeTunnelState (line 39) and the isPortBound/polling loop in tunnel up (lines 67–76, 218–234).
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/cli/tunnel-cli.ts
Line: 39-40

Comment:
**`STATE_DIR` not guaranteed to exist before write**

`fs.writeFileSync` will throw `ENOENT` if `STATE_DIR` (`~/.openclaw`) does not yet exist — for example, on a fresh install before any gateway has been started. The directory should be created before writing.

```suggestion
function writeTunnelState(state: TunnelState): void {
  fs.mkdirSync(STATE_DIR, { recursive: true });
  fs.writeFileSync(tunnelPidPath(), JSON.stringify(state, null, 2), "utf-8");
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/cli/tunnel-cli.ts
Line: 218-234

Comment:
**False-positive success when the target port is already in use**

If something else is already listening on `port` when `tunnel up` runs, SSH exits quickly due to `ExitOnForwardFailure=yes`. However, between spawn and that exit there is a small window (~0–150 ms) where:

- `isPidAlive(child.pid)` still returns `true` (process not yet reaped)
- `isPortBound(port)` returns `true` (the *other* process owns the port)

The loop breaks with `bound = true` and the stale PID is written to disk, even though the SSH process will immediately die. The tunnel then silently appears active until the next `status` check cleans up the file.

A simple guard is to re-check `isPidAlive` after `isPortBound` confirms the port is taken:

```ts
if (await isPortBound(port)) {
  // Verify the SSH child is still the one holding the port, not a pre-existing listener
  if (!isPidAlive(child.pid)) {
    throw new Error(
      `Port ${port} is already in use by another process.`,
    );
  }
  bound = true;
  break;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/cli/tunnel-cli.ts
Line: 67-76

Comment:
**Non-`EADDRINUSE` server errors silently swallowed**

When `server.listen()` fails with any error other than `EADDRINUSE` (e.g., `EACCES` on a privileged port < 1024, or an unexpected OS error), the function resolves `false` — indistinguishable from "port is free". In `tunnel up` this will cause the 8-second wait to expire and then confusingly report that the tunnel failed to bind, rather than surfacing the underlying OS error.

The existing `canConnectLocal()` in `src/infra/ssh-tunnel.ts` is a more reliable signal: a successful TCP **connect** to the port proves the SSH forward is actually accepting connections (not just that *something* is bound). Consider reusing it for the polling loop, and rejecting on unexpected errors:

```ts
server.once("error", (err: NodeJS.ErrnoException) => {
  if (err.code === "EADDRINUSE") {
    resolve(true);
  } else {
    // propagate unexpected errors so callers can surface them
    resolve(false); // or: reject(err) if you want hard failure
  }
});
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: c6fda02

Comment thread src/cli/tunnel-cli.ts
Comment on lines +39 to +40
fs.writeFileSync(tunnelPidPath(), JSON.stringify(state, null, 2), "utf-8");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

STATE_DIR not guaranteed to exist before write

fs.writeFileSync will throw ENOENT if STATE_DIR (~/.openclaw) does not yet exist — for example, on a fresh install before any gateway has been started. The directory should be created before writing.

Suggested change
fs.writeFileSync(tunnelPidPath(), JSON.stringify(state, null, 2), "utf-8");
}
function writeTunnelState(state: TunnelState): void {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(tunnelPidPath(), JSON.stringify(state, null, 2), "utf-8");
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/tunnel-cli.ts
Line: 39-40

Comment:
**`STATE_DIR` not guaranteed to exist before write**

`fs.writeFileSync` will throw `ENOENT` if `STATE_DIR` (`~/.openclaw`) does not yet exist — for example, on a fresh install before any gateway has been started. The directory should be created before writing.

```suggestion
function writeTunnelState(state: TunnelState): void {
  fs.mkdirSync(STATE_DIR, { recursive: true });
  fs.writeFileSync(tunnelPidPath(), JSON.stringify(state, null, 2), "utf-8");
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/cli/tunnel-cli.ts
Comment on lines +218 to +234
` • SSH key auth is configured (no password prompts)\n` +
` • Port ${port} is open on the remote host`,
);
}
if (await isPortBound(port)) {
bound = true;
break;
}
await new Promise((r) => setTimeout(r, 150));
}

if (!bound) {
// Kill it — something went wrong silently
try {
process.kill(child.pid, "SIGKILL");
} catch {
// ignore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

False-positive success when the target port is already in use

If something else is already listening on port when tunnel up runs, SSH exits quickly due to ExitOnForwardFailure=yes. However, between spawn and that exit there is a small window (~0–150 ms) where:

  • isPidAlive(child.pid) still returns true (process not yet reaped)
  • isPortBound(port) returns true (the other process owns the port)

The loop breaks with bound = true and the stale PID is written to disk, even though the SSH process will immediately die. The tunnel then silently appears active until the next status check cleans up the file.

A simple guard is to re-check isPidAlive after isPortBound confirms the port is taken:

if (await isPortBound(port)) {
  // Verify the SSH child is still the one holding the port, not a pre-existing listener
  if (!isPidAlive(child.pid)) {
    throw new Error(
      `Port ${port} is already in use by another process.`,
    );
  }
  bound = true;
  break;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/tunnel-cli.ts
Line: 218-234

Comment:
**False-positive success when the target port is already in use**

If something else is already listening on `port` when `tunnel up` runs, SSH exits quickly due to `ExitOnForwardFailure=yes`. However, between spawn and that exit there is a small window (~0–150 ms) where:

- `isPidAlive(child.pid)` still returns `true` (process not yet reaped)
- `isPortBound(port)` returns `true` (the *other* process owns the port)

The loop breaks with `bound = true` and the stale PID is written to disk, even though the SSH process will immediately die. The tunnel then silently appears active until the next `status` check cleans up the file.

A simple guard is to re-check `isPidAlive` after `isPortBound` confirms the port is taken:

```ts
if (await isPortBound(port)) {
  // Verify the SSH child is still the one holding the port, not a pre-existing listener
  if (!isPidAlive(child.pid)) {
    throw new Error(
      `Port ${port} is already in use by another process.`,
    );
  }
  bound = true;
  break;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/cli/tunnel-cli.ts
Comment on lines +67 to +76
});
server.listen(port, "127.0.0.1");
});
}

async function killPid(pid: number): Promise<void> {
try {
process.kill(pid, "SIGTERM");
} catch {
return; // already gone

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-EADDRINUSE server errors silently swallowed

When server.listen() fails with any error other than EADDRINUSE (e.g., EACCES on a privileged port < 1024, or an unexpected OS error), the function resolves false — indistinguishable from "port is free". In tunnel up this will cause the 8-second wait to expire and then confusingly report that the tunnel failed to bind, rather than surfacing the underlying OS error.

The existing canConnectLocal() in src/infra/ssh-tunnel.ts is a more reliable signal: a successful TCP connect to the port proves the SSH forward is actually accepting connections (not just that something is bound). Consider reusing it for the polling loop, and rejecting on unexpected errors:

server.once("error", (err: NodeJS.ErrnoException) => {
  if (err.code === "EADDRINUSE") {
    resolve(true);
  } else {
    // propagate unexpected errors so callers can surface them
    resolve(false); // or: reject(err) if you want hard failure
  }
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/tunnel-cli.ts
Line: 67-76

Comment:
**Non-`EADDRINUSE` server errors silently swallowed**

When `server.listen()` fails with any error other than `EADDRINUSE` (e.g., `EACCES` on a privileged port < 1024, or an unexpected OS error), the function resolves `false` — indistinguishable from "port is free". In `tunnel up` this will cause the 8-second wait to expire and then confusingly report that the tunnel failed to bind, rather than surfacing the underlying OS error.

The existing `canConnectLocal()` in `src/infra/ssh-tunnel.ts` is a more reliable signal: a successful TCP **connect** to the port proves the SSH forward is actually accepting connections (not just that *something* is bound). Consider reusing it for the polling loop, and rejecting on unexpected errors:

```ts
server.once("error", (err: NodeJS.ErrnoException) => {
  if (err.code === "EADDRINUSE") {
    resolve(true);
  } else {
    // propagate unexpected errors so callers can surface them
    resolve(false); // or: reject(err) if you want hard failure
  }
});
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6fda02c67

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cli/tunnel-cli.ts
Comment on lines +222 to +224
if (await isPortBound(port)) {
bound = true;
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Require a free local port before marking tunnel active

The startup loop treats any EADDRINUSE as proof that the new SSH tunnel is ready, but isPortBound cannot distinguish the SSH child from an unrelated listener. If something is already bound to --port (for example an existing local gateway on 18789), the first iteration can hit isPidAlive(child.pid) while SSH is still alive, then immediately set bound = true and report success even though SSH exits right after ExitOnForwardFailure. This produces a false "Tunnel active" result and writes stale state.

Useful? React with 👍 / 👎.

Comment thread src/cli/tunnel-cli.ts
defaultRuntime.log(
`${colorize(rich, theme.muted, "→")} Stopping tunnel (PID ${state.pid})…`,
);
await killPid(state.pid);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Verify PID ownership before stopping tunnel processes

tunnel down sends signals to any live process with the stored PID, but the file only stores a numeric PID and never verifies it is still the SSH tunnel process. Once the original tunnel exits and the OS reuses that PID, running openclaw tunnel down can terminate an unrelated user process; status also treats that reused PID as alive, so stale state is not self-correcting in this case.

Useful? React with 👍 / 👎.

Comment thread src/cli/tunnel-cli.ts
}

function writeTunnelState(state: TunnelState): void {
fs.writeFileSync(tunnelPidPath(), JSON.stringify(state, null, 2), "utf-8");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Create the state directory before writing tunnel metadata

writeTunnelState writes directly to STATE_DIR/tunnel.pid.json without ensuring the directory exists. In fresh profiles or when OPENCLAW_STATE_DIR points to a new path, tunnel up can successfully start the detached SSH process and then throw ENOENT here, leaving a running tunnel without a PID file so users cannot manage it via tunnel down/status.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Keep open: the feature direction is useful, but this PR is not merge-ready because it adds detached SSH/PID lifecycle management with concrete safety bugs, duplicates existing SSH helper behavior, lacks real behavior proof, and needs a current-main CLI registration rebase.

Canonical path: Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review.

So I’m closing this here because the remaining work is already tracked in the canonical issue.

Review details

Best possible solution:

Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review.

Do we have a high-confidence way to reproduce the issue?

Yes for the blocking PR defects: source inspection of the added file shows the state-dir write failure path, occupied-port false-ready path, PID-only stop path, and hard-coded SSH executable. I did not run a live SSH tunnel in this read-only review.

Is this the best way to solve the issue?

No. The feature may be useful, but this patch should rebase onto current CLI descriptors and share or extract the existing SSH helper's safer startup behavior plus persistent-process identity checks.

Security review:

Security review needs attention: The diff introduces detached SSH/PID lifecycle management with a concrete local process-safety issue.

  • [medium] PID-only tunnel stop can kill unrelated processes — src/cli/tunnel-cli.ts:291
    The persisted state stores only a numeric PID, and tunnel down signals that PID without checking executable, argv, start time, or another ownership token. PID reuse can turn a stale tunnel state file into termination of an unrelated local process.
    Confidence: 0.92

What I checked:

  • stale F-rated PR: PR was opened 2026-03-14T23:36:34Z, is older than 60 days, and the latest review rated it F.
  • proof blocker: real behavior proof is missing and proof tier is F, so this branch is not merge-ready without contributor follow-up.
  • no human follow-up: live comments and timeline hydrated by apply contain no non-automation activity after the ClawSweeper review.

Likely related people:

  • steipete: Peter Steinberger introduced and repeatedly hardened the SSH tunnel/config paths and also owns the current descriptor-catalog shape that this PR must rebase onto. (role: feature-history owner and recent area contributor; confidence: high; commits: 1ec1f6dcbf95, 06289b36da72, 49d0def6d1e8; files: src/infra/ssh-tunnel.ts, src/infra/ssh-config.ts, src/cli/program/subcli-descriptors.ts)
  • vincentkoc: Vincent Koc recently changed the gateway SSH status helper loading path, which is adjacent to the SSH helper and CLI status surfaces this PR reuses or duplicates. (role: recent adjacent contributor; confidence: medium; commits: ca6dbc0f0aca, 3d5af14984ac; files: src/commands/gateway-status.ts, src/agents/sandbox/ssh.ts)
  • joshavant: Josh Avant worked on gateway credential and SecretRef behavior tied to the related node-host remote-auth issue that this tunnel feature overlaps with. (role: adjacent auth contract contributor; confidence: medium; commits: a2cb81199e22, 72cf9253fcb5; files: src/commands/gateway-status/probe-run.ts, src/gateway/credentials.ts, src/gateway/connection-auth.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against f6204d081fc3.

@clawsweeper clawsweeper Bot added the P2 Normal backlog priority with limited blast radius. label May 16, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 16, 2026
@clawsweeper clawsweeper Bot added impact:security Security boundary, credential, authz, sandbox, or sensitive-data risk. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. and removed impact:security Security boundary, credential, authz, sandbox, or sensitive-data risk. labels May 17, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@clawsweeper clawsweeper Bot added the merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. label May 23, 2026
@clawsweeper

clawsweeper Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes docs Improvements or additions to documentation merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant