detached e2f2092

fix(no-yolo): don't auto-refuse tool calls in non-interactive modes

Previously --no-yolo in -p / --json / rpc modes auto-refused every
tool call. That made the flag dangerous to pass to scripts: a
single --no-yolo in a shell config or wrapper script would silently
break any tool-using prompt.

New behaviour:
  - Default: every mode is yolo (tools run freely, no prompts).
  - --no-yolo + interactive TUI: confirm dialog before each tool.
  - --no-yolo + -p / --json / rpc: stderr warning and ignore the
    flag. Tools run freely; scripts keep working.

The TUI confirm dialog and /yolo runtime toggle still work as
before. Also removed the unused wireNoYoloAutoRefuse helper and
simplified core.NewConfirmGate's doc comment.
commit
e2f209247883cbc51baf392249c8ccdcd5717af1
author
patriceckhart · · 2026-04-19 17:17 UTC
parent
ac6d556
signature
unsigned
refs
tag: v0.0.43
5 files · +22 -36
M README.md +1 -1
@@ -152,7 +152,7 @@ zot --help
152152 | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. |
153153 | `--with-skills` | Also load user-installed skills. Without this, only the built-in skills shipped in the binary are loaded. |
154154 | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. |
155-| `--no-yolo` | Confirm every tool call before it runs. In the TUI, a dialog shows the tool name and a one-line preview of its args with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. In print / json / rpc modes (no interactive prompt) every tool call is auto-refused with a reason the model can learn from. Type `/yolo` in the TUI to disable the gate for the rest of the session. |
155+| `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. Type `/yolo` in the TUI to disable the gate for the rest of the session. |
156156
157157 ## Tools
158158
@@ -152,7 +152,7 @@ zot --help
152 | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. |152 | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. |
153 | `--with-skills` | Also load user-installed skills. Without this, only the built-in skills shipped in the binary are loaded. |153 | `--with-skills` | Also load user-installed skills. Without this, only the built-in skills shipped in the binary are loaded. |
154 | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. |154 | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. |
155-| `--no-yolo` | Confirm every tool call before it runs. In the TUI, a dialog shows the tool name and a one-line preview of its args with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. In print / json / rpc modes (no interactive prompt) every tool call is auto-refused with a reason the model can learn from. Type `/yolo` in the TUI to disable the gate for the rest of the session. |155+| `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. Type `/yolo` in the TUI to disable the gate for the rest of the session. |
156 156
157 ## Tools157 ## Tools
158 158
M internal/agent/args.go +8 -5
@@ -69,9 +69,12 @@ type Args struct {
6969 // and waits for an explicit yes/no. The user can also pick
7070 // "always for this tool this session" or "always for anything
7171 // this session" to stop being prompted again. Defaults off
72- // (yolo mode): tools run without asking. In -p / --json / rpc
73- // modes there's no interactive TUI, so --no-yolo auto-refuses
74- // every tool call with a reason the model can learn from.
72+ // (yolo mode): tools run without asking.
73+ //
74+ // No effect in -p / --json / rpc modes, which have no
75+ // interactive prompt. A warning is printed to stderr on startup
76+ // so scripts know the flag is ignored, but tools still run
77+ // freely so automated workflows keep working.
7578 NoYolo bool
7679
7780 ListModels bool
@@ -287,8 +290,8 @@ flags:
287290 default: only built-in skills load
288291
289292 --no-yolo ask before running every tool call
290- (interactive mode only; in -p / --json
291- / rpc this refuses every tool call)
293+ (interactive tui only; ignored with
294+ a stderr warning in -p / --json / rpc)
292295
293296 --max-steps N agent loop iteration cap (default 50)
294297 --list-models print known models and exit
@@ -69,9 +69,12 @@ type Args struct {
69 // and waits for an explicit yes/no. The user can also pick69 // and waits for an explicit yes/no. The user can also pick
70 // "always for this tool this session" or "always for anything70 // "always for this tool this session" or "always for anything
71 // this session" to stop being prompted again. Defaults off71 // this session" to stop being prompted again. Defaults off
72- // (yolo mode): tools run without asking. In -p / --json / rpc72+ // (yolo mode): tools run without asking.
73- // modes there's no interactive TUI, so --no-yolo auto-refuses73+ //
74- // every tool call with a reason the model can learn from.74+ // No effect in -p / --json / rpc modes, which have no
75+ // interactive prompt. A warning is printed to stderr on startup
76+ // so scripts know the flag is ignored, but tools still run
77+ // freely so automated workflows keep working.
75 NoYolo bool78 NoYolo bool
76 79
77 ListModels bool80 ListModels bool
@@ -287,8 +290,8 @@ flags:
287 default: only built-in skills load290 default: only built-in skills load
288 291
289 --no-yolo ask before running every tool call292 --no-yolo ask before running every tool call
290- (interactive mode only; in -p / --json293+ (interactive tui only; ignored with
291- / rpc this refuses every tool call)294+ a stderr warning in -p / --json / rpc)
292 295
293 --max-steps N agent loop iteration cap (default 50)296 --max-steps N agent loop iteration cap (default 50)
294 --list-models print known models and exit297 --list-models print known models and exit
M internal/agent/cli.go +6 -25
@@ -184,12 +184,14 @@ func Run(rawArgs []string, version string) error {
184184 // ---- print / json modes: require credentials, run single-shot ----
185185
186186 func runPrintMode(ctx context.Context, args Args, version string) error {
187+ if args.NoYolo {
188+ fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in print mode (no interactive prompt available); tools will run without confirmation")
189+ }
187190 r, err := Resolve(args, true)
188191 if err != nil {
189192 return err
190193 }
191194 ag := r.NewAgent()
192- wireNoYoloAutoRefuse(ag, args)
193195 sess, _ := openOrCreateSession(args, r, ag, version)
194196 defer sess.Close()
195197
@@ -208,36 +210,15 @@ func runPrintMode(ctx context.Context, args Args, version string) error {
208210 return err
209211 }
210212
211-// wireNoYoloAutoRefuse installs a BeforeToolExecute hook that
212-// refuses every tool call when --no-yolo is active but there's no
213-// interactive UI to prompt (print / json / rpc modes). The reason
214-// is written in a way the model can learn from so it proposes a
215-// different action rather than looping on the same tool.
216-func wireNoYoloAutoRefuse(ag *core.Agent, args Args) {
217- if !args.NoYolo || ag == nil {
218- return
219- }
220- gate := core.NewConfirmGate(nil) // nil inner = auto-refuse
221- prev := ag.BeforeToolExecute
222- ag.BeforeToolExecute = func(call provider.ToolCallBlock) (bool, string, json.RawMessage) {
223- ok, reason, _ := gate.Check(call.Name, core.BuildPreview(call.Arguments, 120))
224- if !ok {
225- return false, reason, nil
226- }
227- if prev != nil {
228- return prev(call)
229- }
230- return true, "", nil
231- }
232-}
233-
234213 func runJSONMode(ctx context.Context, args Args, version string) error {
214+ if args.NoYolo {
215+ fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in json mode (no interactive prompt available); tools will run without confirmation")
216+ }
235217 r, err := Resolve(args, true)
236218 if err != nil {
237219 return err
238220 }
239221 ag := r.NewAgent()
240- wireNoYoloAutoRefuse(ag, args)
241222 sess, _ := openOrCreateSession(args, r, ag, version)
242223 defer sess.Close()
243224
@@ -184,12 +184,14 @@ func Run(rawArgs []string, version string) error {
184 // ---- print / json modes: require credentials, run single-shot ----184 // ---- print / json modes: require credentials, run single-shot ----
185 185
186 func runPrintMode(ctx context.Context, args Args, version string) error {186 func runPrintMode(ctx context.Context, args Args, version string) error {
187+ if args.NoYolo {
188+ fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in print mode (no interactive prompt available); tools will run without confirmation")
189+ }
187 r, err := Resolve(args, true)190 r, err := Resolve(args, true)
188 if err != nil {191 if err != nil {
189 return err192 return err
190 }193 }
191 ag := r.NewAgent()194 ag := r.NewAgent()
192- wireNoYoloAutoRefuse(ag, args)
193 sess, _ := openOrCreateSession(args, r, ag, version)195 sess, _ := openOrCreateSession(args, r, ag, version)
194 defer sess.Close()196 defer sess.Close()
195 197
@@ -208,36 +210,15 @@ func runPrintMode(ctx context.Context, args Args, version string) error {
208 return err210 return err
209 }211 }
210 212
211-// wireNoYoloAutoRefuse installs a BeforeToolExecute hook that
212-// refuses every tool call when --no-yolo is active but there's no
213-// interactive UI to prompt (print / json / rpc modes). The reason
214-// is written in a way the model can learn from so it proposes a
215-// different action rather than looping on the same tool.
216-func wireNoYoloAutoRefuse(ag *core.Agent, args Args) {
217- if !args.NoYolo || ag == nil {
218- return
219- }
220- gate := core.NewConfirmGate(nil) // nil inner = auto-refuse
221- prev := ag.BeforeToolExecute
222- ag.BeforeToolExecute = func(call provider.ToolCallBlock) (bool, string, json.RawMessage) {
223- ok, reason, _ := gate.Check(call.Name, core.BuildPreview(call.Arguments, 120))
224- if !ok {
225- return false, reason, nil
226- }
227- if prev != nil {
228- return prev(call)
229- }
230- return true, "", nil
231- }
232-}
233-
234 func runJSONMode(ctx context.Context, args Args, version string) error {213 func runJSONMode(ctx context.Context, args Args, version string) error {
214+ if args.NoYolo {
215+ fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in json mode (no interactive prompt available); tools will run without confirmation")
216+ }
235 r, err := Resolve(args, true)217 r, err := Resolve(args, true)
236 if err != nil {218 if err != nil {
237 return err219 return err
238 }220 }
239 ag := r.NewAgent()221 ag := r.NewAgent()
240- wireNoYoloAutoRefuse(ag, args)
241 sess, _ := openOrCreateSession(args, r, ag, version)222 sess, _ := openOrCreateSession(args, r, ag, version)
242 defer sess.Close()223 defer sess.Close()
243 224
M internal/agent/rpc.go +3 -0
@@ -40,6 +40,9 @@ import (
4040 // Auth: if $ZOTCORE_RPC_TOKEN is set, the first command must be
4141 // {"type":"hello","token":"..."} or the connection is closed.
4242 func runRPCMode(ctx context.Context, args Args, version string) error {
43+ if args.NoYolo {
44+ fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in rpc mode (no interactive prompt available); tools will run without confirmation")
45+ }
4346 r, err := Resolve(args, true)
4447 if err != nil {
4548 return err
@@ -40,6 +40,9 @@ import (
40 // Auth: if $ZOTCORE_RPC_TOKEN is set, the first command must be40 // Auth: if $ZOTCORE_RPC_TOKEN is set, the first command must be
41 // {"type":"hello","token":"..."} or the connection is closed.41 // {"type":"hello","token":"..."} or the connection is closed.
42 func runRPCMode(ctx context.Context, args Args, version string) error {42 func runRPCMode(ctx context.Context, args Args, version string) error {
43+ if args.NoYolo {
44+ fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in rpc mode (no interactive prompt available); tools will run without confirmation")
45+ }
43 r, err := Resolve(args, true)46 r, err := Resolve(args, true)
44 if err != nil {47 if err != nil {
45 return err48 return err
M internal/core/confirm.go +4 -5
@@ -53,11 +53,10 @@ type ConfirmGate struct {
5353 allowedTool map[string]bool
5454 }
5555
56-// NewConfirmGate returns a gate backed by inner. When inner is nil,
57-// the gate operates in auto-refuse mode: every tool call is denied
58-// with a fixed reason. That's what non-interactive modes (-p /
59-// --json / rpc) use when --no-yolo is on, since they have no way
60-// to prompt the user.
56+// NewConfirmGate returns a gate backed by inner. Inner can be nil;
57+// in that case every not-yet-allowed tool call is refused with a
58+// fixed reason (the gate is effectively a blocker until AllowAll /
59+// SetConfirmer is called).
6160 func NewConfirmGate(inner Confirmer) *ConfirmGate {
6261 return &ConfirmGate{
6362 inner: inner,
@@ -53,11 +53,10 @@ type ConfirmGate struct {
53 allowedTool map[string]bool53 allowedTool map[string]bool
54 }54 }
55 55
56-// NewConfirmGate returns a gate backed by inner. When inner is nil,56+// NewConfirmGate returns a gate backed by inner. Inner can be nil;
57-// the gate operates in auto-refuse mode: every tool call is denied57+// in that case every not-yet-allowed tool call is refused with a
58-// with a fixed reason. That's what non-interactive modes (-p /58+// fixed reason (the gate is effectively a blocker until AllowAll /
59-// --json / rpc) use when --no-yolo is on, since they have no way59+// SetConfirmer is called).
60-// to prompt the user.
61 func NewConfirmGate(inner Confirmer) *ConfirmGate {60 func NewConfirmGate(inner Confirmer) *ConfirmGate {
62 return &ConfirmGate{61 return &ConfirmGate{
63 inner: inner,62 inner: inner,

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close