Skip to content

Clear stale yolo approval state on no-launch reruns - #6868

Merged
danielhanchen merged 9 commits into
mainfrom
fix-yolo-no-launch-persistence
Jul 7, 2026
Merged

danielhanchen merged 9 commits into
mainfrom
fix-yolo-no-launch-persistence

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Problem

unsloth start <agent> --no-launch reuses a stable Unsloth-owned config directory across runs (by design, since a previously printed recipe may still be running an agent whose sessions live there). The --yolo path writes auto-approval settings into those persistent files, but runs without --yolo never removed them:

  • OpenClaw: tools.exec = {host: gateway, security: full, ask: off} in openclaw.json, plus exec-approvals.json with security=full/ask=off defaults.
  • OpenCode: permission = {edit: allow, bash: allow, webfetch: allow} in opencode.json.

So after a single --yolo --no-launch run, every later --no-launch run of the same agent without --yolo still had tool execution silently pre-approved. That defeats the confirmation gate the non-yolo mode is supposed to provide: any prompt-injected or otherwise malicious instruction the agent follows can run commands or edit files without the user ever being asked.

Repro on main:

unsloth start opencode --no-launch --yolo   # writes permission allow block
unsloth start opencode --no-launch          # block is still there

The other agents (claude, codex, hermes, pi) are unaffected: their yolo form is a command line flag, nothing is persisted. Launch mode is unaffected: it uses an ephemeral temp dir that is deleted when the agent exits.

Fix

Runs without --yolo now reset the persisted state instead of leaving it behind:

  • OpenClaw: the host/security/ask keys the yolo path writes are stripped from tools.exec (empty parents removed), and the yolo defaults are stripped from exec-approvals.json. Approvals OpenClaw itself recorded in that file are preserved; the file is deleted when only the yolo payload is left. The CLI echoes Removed .../exec-approvals.json so the reset is visible.
  • OpenCode: the permission block is dropped.

Any other user or runtime state in the session dir is left alone, so the never-wipe contract of the no-launch dir still holds.

Validation

  • Regression tests added for both agents at the CLI level (--yolo --no-launch then --no-launch on the same dir leaves no auto-approval state, session provider intact) and at the writer level, including one that checks OpenClaw runtime-recorded approvals survive the reset and one for foreign tools.exec keys. Full suite: 138 passed.
  • Live check against a running Studio: unsloth start openclaw/opencode --no-launch --yolo followed by the same command without --yolo on a fresh Studio home. Before the fix the exec policy, approvals file, and permission block all persist; after the fix the rerun removes all three and the printed recipe still works.

The no-launch session config dir is deliberately reused across runs, but
the config writers only ever added the --yolo auto-approval settings and
never removed them. After one --yolo --no-launch run, every later run
without --yolo kept OpenClaw's tools.exec security=full/ask=off policy
plus exec-approvals.json, and OpenCode's permission allow block, so tool
execution stayed silently pre-approved.

Non-yolo runs now reset that state: OpenClaw drops the exec policy keys
and the yolo defaults in exec-approvals.json (approvals OpenClaw itself
recorded are kept; the file is removed when only the yolo payload is
left), and OpenCode drops the permission block. Launch mode is untouched
since it already uses an ephemeral temp dir.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces state cleanup for OpenClaw and OpenCode configurations when running without the --yolo flag, ensuring that auto-approval settings left behind by previous --yolo runs are correctly stripped. It also adds comprehensive unit tests to verify this behavior. The reviewer identified a potential issue where an unparseable exec-approvals.json file could be silently deleted due to a fallback to an empty dictionary, and suggested checking if the parsed state is not None before proceeding.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1064 to +1073
state = _read_json_object(approvals) or {}
had_defaults = state.pop("defaults", None) is not None
if set(state) <= {"version"}:
# Nothing left but our own yolo payload (or unreadable): remove it.
approvals.unlink()
typer.echo(f"Removed {approvals}")
elif had_defaults:
# Keep approvals OpenClaw itself recorded; only the yolo defaults go.
_write_private_json(approvals, state)
typer.echo(f"Updated {approvals}")

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.

medium

If _read_json_object(approvals) returns None (e.g., due to a JSON parsing error or OSError), fallback to {} via or {} causes set(state) <= {"version"} to evaluate to True. This results in the silent deletion of exec-approvals.json via approvals.unlink().

Following the design of _read_json_object (which returns None specifically so that callers can leave user-managed files untouched rather than clobbering or deleting them), we should check if state is not None before proceeding, preserving the file if it is unparseable.

Suggested change
state = _read_json_object(approvals) or {}
had_defaults = state.pop("defaults", None) is not None
if set(state) <= {"version"}:
# Nothing left but our own yolo payload (or unreadable): remove it.
approvals.unlink()
typer.echo(f"Removed {approvals}")
elif had_defaults:
# Keep approvals OpenClaw itself recorded; only the yolo defaults go.
_write_private_json(approvals, state)
typer.echo(f"Updated {approvals}")
state = _read_json_object(approvals)
if state is not None:
had_defaults = state.pop("defaults", None) is not None
if set(state) <= {"version"}:
# Nothing left but our own yolo payload: remove it.
approvals.unlink()
typer.echo(f"Removed {approvals}")
elif had_defaults:
# Keep approvals OpenClaw itself recorded; only the yolo defaults go.
_write_private_json(approvals, state)
typer.echo(f"Updated {approvals}")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in cc60098: an unparseable exec-approvals.json is now left in place (with the cleanup skipped), matching how an unparseable config is handled elsewhere in this file. Test added.

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

Copy link
Copy Markdown

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: b4ff621301

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1056 to +1057
for field in ("host", "security", "ask"):
exec_policy.pop(field, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve non-yolo OpenClaw exec policies

When the reused no-launch config already contains an OpenClaw exec policy that was not written by --yolo (for example a stricter security value or ask: "on"), this loop removes those fields solely because their names overlap with the yolo payload. The surrounding writer otherwise merges and preserves existing config, so a plain unsloth start openclaw --no-launch can silently discard a user's/runtime exec policy; only remove the exact yolo values that this command wrote.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in cc60098: each exec policy field is now removed only when it carries the exact value the yolo path writes (host=gateway, security=full, ask=off), so stricter or hand-set policies survive. Test added.

Comment thread unsloth_cli/commands/start.py Outdated
else:
# The no-launch config is reused across runs; drop a permission block left by
# a previous --yolo run so this session prompts again.
config.pop("permission", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve custom OpenCode permission blocks

A no-yolo rerun now deletes any existing OpenCode permission block, not just the auto-allow block written by a previous --yolo run. In the persistent no-launch config this can erase user/session permissions such as denied tools or ask-only policies during an ordinary reconnect, despite the writer's merge-preserve behavior elsewhere; gate the cleanup on the block matching the yolo {edit,bash,webfetch}: allow payload.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in cc60098: only edit/bash/webfetch entries whose value is allow are dropped; deny/ask policies and other tools' entries are kept. Test added.

Comment thread unsloth_cli/commands/start.py Outdated
approvals = path.parent / "exec-approvals.json"
if approvals.exists():
state = _read_json_object(approvals) or {}
had_defaults = state.pop("defaults", None) is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve non-yolo OpenClaw approval defaults

OpenClaw's host approvals file stores enforceable defaults, not just the yolo shortcut. In the persistent no-launch state dir, if a user or OpenClaw UI has set non-yolo defaults such as stricter security/ask behavior, this pop removes them on every plain unsloth start openclaw --no-launch and may delete the whole file as stale yolo state; only clear defaults when they match the yolo payload this command wrote.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in cc60098: approvals defaults are now cleared per field and only when they match the yolo payload (security=full, ask=off, askFallback=full); stricter defaults are kept and the file is only removed when nothing but the yolo payload was in it. Test added.

danielhanchen and others added 2 commits July 4, 2026 11:47
Match each field against the exact value the yolo path writes before
removing it, so a stricter exec policy, approvals defaults set by the
user or the OpenClaw UI, and deny/ask OpenCode permission entries all
survive a plain no-launch rerun. An unparseable exec-approvals.json is
left in place, matching how an unparseable config is handled.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

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

Copy link
Copy Markdown

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: 15aca2ac58

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1063 to +1064
if exec_policy.get(field) == yolo_value:
del exec_policy[field]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Write a restrictive OpenClaw policy instead of deleting it

In the openclaw --yolo --no-launch then openclaw --no-launch path, deleting the yolo tools.exec fields does not restore prompting: OpenClaw's gateway/node defaults for omitted exec policy are security=full and ask=off (docs), and the later approvals-file cleanup leaves approvals omitted so they fall back to that permissive policy. With no sandbox, host=auto resolves to gateway, so the regenerated plain recipe can still run exec without approval; reset to an explicit cautious/ask policy and matching approvals defaults instead of dropping these keys.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed against openclaw 2026.6.11: with tools.exec omitted and the approvals file missing, exec-policy show reports the effective policy as security=full, ask=off, so deleting the yolo keys leaves auto-approval on. Fixed in a27d425: a non-yolo run now writes a prompting policy (security=allowlist, ask=on-miss), which prompts even after the approvals file is removed since the stricter of config and host approvals wins. The same applies to OpenCode, which defaults an unset permission to allow, so non-yolo now writes edit/bash/webfetch=ask instead of dropping the block.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correct, thank you. Verified against OpenClaw's own docs: an omitted tools.exec resolves to security=full, ask=off on the gateway host (sandboxing is off by default, so host=auto is the gateway), so deleting the keys keeps auto-approval on. Fixed in a27d425: a non-yolo run now writes an explicit prompting policy (security=allowlist, ask=on-miss) instead of deleting, replacing only a permissive/yolo policy and leaving a stricter hand-set one alone. The same permissive default applies to OpenCode (an unset permission defaults to allow), so its non-yolo path now writes edit/bash/webfetch=ask too. The file-edit CI drives these two agents with --yolo so the headless smoke test still runs tools.

…ive default

OpenClaw and OpenCode both treat an omitted policy as permissive: OpenClaw's
effective exec policy for an unset tools.exec is security=full/ask=off on the
gateway host, and OpenCode defaults an unset permission to allow. So clearing
the yolo values on a non-yolo run did not restore prompting, it fell back to
those permissive defaults and left tool execution auto-approved.

A non-yolo run now writes an explicit prompting policy: OpenClaw gets
security=allowlist/ask=on-miss (verified to prompt even with the approvals file
removed, since the stricter of config and approvals wins), and OpenCode gets
edit/bash/webfetch=ask. Only a permissive/yolo value is tightened; a stricter
deny (or an ask the user set) is preserved, and the yolo approvals defaults are
still stripped. The file-edit CI path opts opencode/openclaw into --yolo, since
those agents now prompt by default and the headless test needs auto-approval.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

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

Copy link
Copy Markdown

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: a27d425243

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1142 to +1144
permission = config.get("permission")
permission = permission if isinstance(permission, dict) else {}
to_ask = [t for t in ("edit", "bash", "webfetch") if permission.get(t, "allow") == "allow"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve global OpenCode permission rules

When an existing OpenCode config uses a valid global permission rule such as "permission": "deny" or {"*": "deny"}, this reset path treats it as unset/default-allow and adds edit/bash/webfetch: "ask", replacing the string form entirely or overriding the catch-all. OpenCode's permissions docs allow both a single action value and * catch-all rules, so a non-yolo rerun can silently weaken stricter user-managed permissions instead of preserving them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed against opencode 1.17.13 (a string permission normalizes to {"": value}, and a per-tool key overrides the catch-all). The reset now leaves a string permission in the config file as-is, treats a "" catch-all as the default an absent tool inherits, and only tightens an EFFECTIVE "allow" to "ask", so a "deny" string or {"*":"deny"} is no longer weakened (4681ac4).

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1056 to +1060
if exec_policy.get("security", "full") == "full" and exec_policy.get("ask", "off") == "off":
exec_policy = _subdict(_subdict(config, "tools"), "exec")
exec_policy.pop("host", None) # routing only; defaults to the gateway host
exec_policy["security"] = "allowlist" # only allowlisted commands skip approval
exec_policy["ask"] = "on-miss" # prompt on every non-allowlisted command

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect OpenClaw exec mode before adding fields

If the existing OpenClaw config uses the normalized tools.exec.mode knob (for example mode: "deny" or mode: "ask"), this branch treats the absent explicit security/ask fields as the permissive defaults and writes security plus ask alongside mode. OpenClaw's exec config documents that mode derives these fields and cannot be combined with explicit tools.exec.security/tools.exec.ask, so a non-yolo rerun can corrupt or relax an existing stricter policy rather than preserving it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, confirmed against the 2026.6.11 binary: with tools.exec.mode set, adding explicit security/ask makes OpenClaw reject the whole config ("tools.exec.mode cannot be combined with tools.exec.security or tools.exec.ask"), and it downgrades a stricter mode:deny/ask policy. The non-yolo reset now leaves any mode-based policy untouched (4681ac4).

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1145 to +1148
if to_ask:
permission = _subdict(config, "permission")
for tool in to_ask:
permission[tool] = "ask"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce non-yolo OpenCode permissions above project config

This only writes the new ask policy into the OPENCODE_CONFIG file, but OpenCode loads that custom config before project opencode.json and only OPENCODE_CONFIG_CONTENT has higher precedence. In a repo whose project config allows edit, bash, or webfetch, a plain non-yolo recipe still gets those project-level auto-approvals because the inline config for non-yolo only pins model; the reset needs to carry the ask policy at the same precedence as yolo does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed with opencode debug config: with the ask policy only in OPENCODE_CONFIG (which loads below project opencode.json), a project config allowing edit/bash/webfetch still resolved to allow, so a non-yolo session auto-approved. write_opencode_config now returns the session permission and the callback carries it in OPENCODE_CONFIG_CONTENT (above project config) for non-yolo too, mirroring yolo, so a non-yolo session prompts. It never weakens a stricter user rule: the inline policy is deny when the user set deny, ask otherwise (4681ac4).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Kept and reinforced in a5781bc. The non-yolo ask policy rides in OPENCODE_CONFIG_CONTENT (verified highest precedence, per-tool deep merge, higher-layer-wins), so a project config that allows edit/bash/webfetch still prompts. To reconcile with the related finding that this must not weaken a project deny, the inline value is now the stricter of "ask" and the value in our own session config: a deny or a pure deny/ask object we can see is carried inline verbatim, anything looser floors to "ask". A permissive project always prompts, and no path can produce an inline "allow" (silent auto-approve).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Revisiting this after the deeper review: I have reverted the inline permission override for non-yolo runs in 249dbc6. Since OPENCODE_CONFIG_CONTENT outranks a project opencode.json that we cannot read, no value forced there can prompt over a permissive project without also weakening a stricter project rule (deny), a global string, a catch-all-less granular object, or a per-agent permission. The persistence bug is fully addressed by clearing our own config (flipping the explicit yolo allow back to ask); a project config is the user own deliberate choice and is now honored as written. --yolo still carries its allow inline so it works over a project config.

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1056 to +1058
if exec_policy.get("security", "full") == "full" and exec_policy.get("ask", "off") == "off":
exec_policy = _subdict(_subdict(config, "tools"), "exec")
exec_policy.pop("host", None) # routing only; defaults to the gateway host

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't treat sandbox-only exec config as yolo

When a user config has tools.exec.host: "sandbox" with no explicit security, OpenClaw's documented default is sandbox security=deny, not the gateway/node full default. This condition still interprets the missing field as full and then removes host, so a non-yolo rerun can broaden a sandbox-deny setup into gateway/auto exec with allowlist prompts instead of preserving the stricter sandbox policy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed from exec.md (security default is deny for sandbox, full for gateway/node). --yolo only ever writes host=gateway, so a non-gateway host is user-set. The reset now only rewrites a gateway-routed permissive policy and never pops host when it is sandbox or node, so a sandbox-deny (or node) setup is preserved instead of being broadened to gateway/auto (4681ac4).

danielhanchen and others added 2 commits July 5, 2026 18:01
…rules on non-yolo reset

The non-yolo reset for openclaw/opencode assumed an omitted policy was the
permissive yolo default and rewrote it, which corrupted or weakened stricter
setups it should have preserved:

- OpenClaw tools.exec.mode is the normalized policy knob and cannot be combined
  with explicit security/ask (OpenClaw rejects the whole config), so writing
  security+ask alongside a mode:deny/ask policy both broke the config and
  relaxed it. Leave a mode-based policy untouched.
- host=sandbox defaults to security=deny and host=node routes to a paired node;
  neither is written by --yolo (which only writes host=gateway). Treating the
  missing security as full and popping host broadened those into gateway/auto
  exec. Only rewrite a gateway-routed permissive policy, and never pop a
  non-gateway host.
- OpenCode permission can be a string ("deny") or a {"*": ...} catch-all.
  The old code dropped a string form and overrode a catch-all by writing
  per-tool ask, weakening a stricter user rule. Now a string is left in place,
  a catch-all governs absent tools, and only an effective allow is tightened.
- The non-yolo ask policy only lived in OPENCODE_CONFIG, which loads below
  project opencode.json, so a project config allowing edit/bash/webfetch still
  auto-approved. Carry the ask policy in OPENCODE_CONFIG_CONTENT (above project
  config) too, symmetric to how yolo carries its allow.

Also harden the openclaw path against a malformed non-dict tools value.

Adds tests for mode/sandbox/node hosts, string and catch-all permissions, and
the inline ask policy over a project config.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

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

Copy link
Copy Markdown

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: 7d815b9160

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1176 to +1178
session_permission = {
t: "deny" if permission.get(t, default) == "deny" else "ask" for t in tools
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve granular OpenCode deny permissions

When the persisted OpenCode config uses the granular object syntax, e.g. permission: {"bash": {"*": "deny"}}, this expression sees a dict and emits "bash": "ask" in OPENCODE_CONFIG_CONTENT. That env config outranks the session/project files, so a user’s deny policy is weakened to an approvable action for non-yolo runs; object-valued rules need to preserve deny/catch-all semantics instead of being collapsed to ask.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right, confirmed with the 1.17.13 binary: an object value like {"bash":{"*":"deny"}} was being collapsed to inline "bash":"ask", and since OPENCODE_CONFIG_CONTENT deep-merges over project config that weakened the deny. Fixed in a5781bc: a granular object that grants nothing (ask/deny only) now rides inline verbatim and is preserved in the file. Note an object that grants "allow" anywhere is floored to the string "ask" instead (a string fully replaces a project object, so no inline allow pattern can leak into a silent auto-approve).

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1066 to +1070
permissive = (
"mode" not in exec_policy
and host in (None, "gateway", "auto")
and exec_policy.get("security", "full") == "full"
and exec_policy.get("ask", "off") == "off"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not broaden implicit sandbox exec policy

When OpenClaw is using its implicit/auto sandbox host, the omitted exec policy is not the gateway's permissive default; the surrounding comment already notes sandbox defaults to security=deny. Treating host in (None, "auto") as yolo-permissive rewrites a fresh non-yolo config to allowlist/on-miss, so users with an active sandbox go from deny-by-default to commands being allowed after a prompt even though there was no stale yolo state to clear.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed against exec.md: with sandbox off, host=auto resolves to gateway (security=full), but with an active sandbox an omitted/auto host defaults to security=deny, so the old predicate would broaden a fresh sandboxed config from deny to allowlist. --yolo only ever writes host=gateway/security=full/ask=off explicitly, so a5781bc scopes the reset to exactly that fingerprint and leaves None/auto/sandbox/node untouched.

Comment on lines +1480 to +1481
if session_permission:
inline_config["permission"] = session_permission

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not override project-level OpenCode denies

For non-yolo runs with the usual generated session policy, this writes {"bash":"ask","edit":"ask","webfetch":"ask"} into OPENCODE_CONFIG_CONTENT, which the adjacent comment says outranks the repo's own opencode.json. In a project that deliberately has permission: {"bash":"deny"} (or denies edits/webfetch), the inline block weakens that deny to an approvable action; the override should only defeat permissive project rules, not stricter ones.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Verified the merge with the 1.17.13 binary: OPENCODE_CONFIG_CONTENT is per-tool deep-merged and higher-layer-wins, not stricter-wins, so an inline "ask" does override a project "bash":"deny". Since we cannot read the project opencode.json, no single inline value both prompts over a permissive project (comment 3524985232) and preserves an unobservable project deny.

Landed one coherent design in a5781bc: the inline value per tool is the stricter of "ask" and the value in our OWN session config. A deny or a pure deny/ask object a user placed in the reused session dir rides inline verbatim, so a stricter rule we can see is never weakened; anything looser (allow, or an object granting allow) floors to "ask". The one residual case, a deny that exists only in the project config, is softened to "ask", a prompt, never a silent auto-approve, which keeps the non-yolo contract intact. Escalating to inline "deny" instead would hard-block tools in a plain repo, a worse default for an interactive session.

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1066 to +1068
permissive = (
"mode" not in exec_policy
and host in (None, "gateway", "auto")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset normalized OpenClaw full mode

OpenClaw's docs define tools.exec.mode: "full" as running host exec without approval prompts, but this predicate skips every mode-based policy, so a config that has been normalized/migrated from the yolo security=full, ask=off form remains in no-approval mode after unsloth start openclaw --no-launch without --yolo. The reset needs to treat mode: "full" as stale yolo state rather than preserving it as a stricter user policy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Checked against the 2026.6.11 binary: OpenClaw never migrates our security=full/ask=off write into mode:"full". Feeding that exact config through config validate, config get tools.exec, and doctor --fix leaves security/ask in place and never introduces a mode key, and mode cannot even coexist with security/ask (the config is rejected). So a mode:"full" is always a deliberate user policy, not stale yolo state, and clobbering it would override a genuine user intent. Left as-is.

…ular denies

OpenClaw: reset only the exact host=gateway/security=full/ask=off policy --yolo
writes, so an omitted or host=auto/sandbox/node policy (which can resolve to a
sandbox security=deny default) is no longer broadened to allowlist/on-miss, and a
deliberate tools.exec.mode is left alone (OpenClaw never migrates our security/ask
write into a mode).

OpenCode: carry a granular object or a deny inline verbatim so a per-tool user rule
is not collapsed to a blanket ask, but floor any object that grants allow anywhere to
the string ask (which fully replaces a project object) so no inline allow pattern can
leak through into a silent auto-approve on a non-yolo session.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

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

Copy link
Copy Markdown

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: a5781bc832

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_cli/commands/start.py Outdated
# through. A plain "allow" or an absent tool also floors to "ask".
if isinstance(value, dict):
# Copy so the returned inline dict never aliases config["permission"].
return copy.deepcopy(value) if not _has_allow(value) else "ask"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add a catch-all before inlining granular permissions

When the stored OpenCode permission is a granular object with only ask/deny entries, this branch copies it into OPENCODE_CONFIG_CONTENT, which is loaded after project config. Per the OpenCode permissions docs, granular rules are just pattern matches and unspecified permissions fall back to permissive defaults, so a session config like {"bash":{"git *":"ask"}} will override a project's bash: "ask" and still auto-approve non-matching commands such as npm test in a non-yolo run. Collapse these objects to "ask" or add a "*": "ask" floor before inlining them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 249dbc6: a non-yolo session no longer inlines any permission, so a granular object with no catch-all is left in our config file as the user wrote it and cannot override a project rule or auto-approve unmatched commands.

Comment thread unsloth_cli/commands/start.py Outdated
Comment on lines +1092 to +1093
if defaults.get(field) == yolo_value:
del defaults[field]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require the full YOLO defaults before deleting fields

This removes any default field whose value happens to match the YOLO payload, even when the approvals file is a user-managed mixed policy rather than stale YOLO state. For example defaults={"security":"allowlist","ask":"on-miss","askFallback":"full"} loses askFallback; OpenClaw's docs state an omitted askFallback defaults to deny, so a plain non-yolo rerun changes headless/no-UI fallback behavior despite not matching the YOLO tuple. Only strip defaults when the full YOLO fingerprint is present.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 249dbc6: the approvals cleanup now strips the defaults only when the full yolo fingerprint (security=full, ask=off, askFallback=full) is present, so a mixed policy that merely shares askFallback=full is kept intact.

Comment thread unsloth_cli/commands/start.py Outdated
# A string permission ("deny"/"ask"/"allow") is a global user rule left in the
# config file as-is; "deny" rides inline verbatim, anything looser floors to
# "ask".
session_permission = {t: permission if permission == "deny" else "ask" for t in tools}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve global string permissions when inlining

When the session config contains a global string policy such as "permission": "deny" or "ask", this collapses that catch-all into only the three yolo-related tools before putting it in OPENCODE_CONFIG_CONTENT. Because the inline config has higher precedence than OPENCODE_CONFIG, the object replaces the original string policy and other permissions fall back to OpenCode's defaults (mostly allow), so a user-managed global deny/ask is weakened on non-yolo runs. Carry the string through unchanged, or inline an equivalent "*" catch-all.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 249dbc6 by removing the non-yolo inline override entirely. A global string permission is now left in our config file untouched and nothing is forced into OPENCODE_CONFIG_CONTENT, so it can no longer be narrowed to the three tools.

if yolo:
inline_config["permission"] = {"edit": "allow", "bash": "allow", "webfetch": "allow"}
if session_permission:
inline_config["permission"] = session_permission

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply non-yolo policy to agent permissions too

This only inlines a top-level permission, but OpenCode also supports per-agent permissions and documents that agent rules are merged with global config and take precedence. In a repo with agent.build.permission allowing bash/edit/webfetch (the default agent is build), a non-yolo unsloth start opencode still auto-approves those tools despite this inline ask policy. Inline or neutralize the active/default agent permission as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 249dbc6: since a non-yolo session no longer forces a top-level permission over the project config, per-agent permissions are honored as written rather than partially overridden.

Comment thread unsloth_cli/commands/start.py Outdated
# through. A plain "allow" or an absent tool also floors to "ask".
if isinstance(value, dict):
# Copy so the returned inline dict never aliases config["permission"].
return copy.deepcopy(value) if not _has_allow(value) else "ask"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep granular deny rules when removing allows

For any granular permission object that contains an allow, this collapses the whole tool policy to the string "ask". A user/session rule like {"bash":{"rm *":"deny","git *":"allow"}} therefore becomes approvable for rm * in the higher-precedence inline config on a non-yolo run, weakening an explicit deny while trying to remove stale allows. Preserve the object and rewrite only its allow entries to ask, with a safe catch-all, instead of replacing the whole rule.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 249dbc6: with the inline override removed, a granular object (including one mixing deny and allow) is left in our config verbatim rather than collapsed, so an explicit deny is never weakened.

danielhanchen and others added 2 commits July 5, 2026 22:33
…ngerprint

The non-yolo OpenCode reset carried a session permission in
OPENCODE_CONFIG_CONTENT, which outranks the project opencode.json we
cannot read. That inline override could not correctly reflect the project:
it weakened a project deny to a prompt, mishandled global string rules,
leaked through a granular object's permissive default when no catch-all
was present, collapsed an object with an allow (losing its deny), and
missed per-agent permissions. All of these stem from forcing a value over
an unknown project config.

A non-yolo run now only undoes what --yolo wrote: it flips our own
explicit per-tool allow back to ask in our config file and carries no
permission inline, so the project's own permissions are honored as
written. Clearing our persisted yolo state is the actual fix; --yolo still
carries its allow inline so it works over a project config.

OpenClaw approvals cleanup now strips the yolo defaults only when the full
fingerprint (security=full, ask=off, askFallback=full) is present, so a
mixed user policy that merely shares askFallback=full (whose omitted
default is deny) is kept intact.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: dad49fceb0

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 7, 2026
@danielhanchen
danielhanchen merged commit 69f8e0b into main Jul 7, 2026
48 checks passed
@danielhanchen
danielhanchen deleted the fix-yolo-no-launch-persistence branch July 7, 2026 07:06
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.

1 participant