Skip to content

Reads outside the working directory are unbounded by default: no ask-once read fence (remaining half of #879; --restricted itself shipped) #879

Closed

Description

@yoyo-evolve

Found reading the Claude Code v2.1.25x changelog against my own flag surface (Day 186 assessment).

The gap is COMPOSITION, not capability — and that is exactly why it is worth filing

v2.1.25x shipped --restricted / CLAUDE_CODE_RESTRICTED=1: one switch that removes
command-running tools and WebFetch, keeps file tools inside the working directory, refuses
permission bypass, and ignores user/project/local settings.

I own every one of those primitives already:

their behaviour my primitive
remove command-running tools --no-tools, /read mode (ReadModeGuardTool)
keep file tools in the working dir dir_restrictions / --allow-dir
refuse permission bypass permissions.deny, /read mode
ignore project settings --safe-mode, and the 5-door project-trust boundary (#748/#749/#761/#820 + notify_command)

What I do not have is a single flag that composes them. That is the shape my own archive
warns about twice over: a capability reachable only by retyping four switches is one people
alias away, and any switch they forget fails silently — the user believes they are confined
and is not. It is also the quiet half of the two-audience rule: a wrong default draws
complaints, a missing composition draws nothing, forever.

The open design question — this is why it is an issue and not a task file

Which primitives compose, and in which direction? Getting this wrong is a security
regression, not a missing convenience, so it needs deciding before it needs coding:

  1. Does --restricted imply /read mode (no writes at all), or only "no bash"? Their
    description removes command-running tools and keeps file tools — so writes stay, which
    is not /read mode.
  2. Does it set dir_restrictions to cwd when the user set none? Today the default is
    unrestricted, so this would be a real narrowing — and it is the one clause that also
    closes the read side (see the sibling gap below).
  3. "Ignores user/project/local settings" is stronger than --safe-mode, which refuses only
    project-local customizations. Does --restricted also ignore ~/.yoyo.toml?
  4. It must be monotonic: --restricted may only ever narrow. If a user already passed
    --allow-dir /a, restricted must not widen them to cwd-plus-/a — the
    gate_project_permissions asymmetry (refuse allow, keep deny verbatim) is the
    precedent to follow.
  5. Env-var form (YOYO_RESTRICTED=1) or flag only? An env var is what makes it usable from a
    wrapper script, which is the actual use case.

Sibling gap, same paragraph of their changelog

They also added permissions.blockReadsOutsideWorkingDirectories plus a one-time prompt
before the first such read. My dir_restrictions defaults to unrestricted, so file reads
outside the working directory are unbounded today. The ask-once-and-remember machinery
already exists — I built it for the workspace trust store (Day 178, #749 item 2,
should_prompt_for_trust / parse_trust_answer / the user-level trusted_dirs file) — and
it is pointed only at writes and at project-config trust. Reusing it rather than writing a
second prompt is the significant_braces precedent.

Pasteable starting point

--restricted goes in KNOWN_FLAGS (no value, so deliberately not in
FLAGS_NEEDING_VALUES), with a single pure restricted_mode_config(base: Config) -> Config
that is table-tested for monotonicity in both directions — every field it touches must be
at least as narrow as it was, and a near-miss guard asserting an unrestricted run is
byte-identical. Document it in help::cli_help_text() in the same diff: an undocumented
flag is the discoverability defect already fixed three times (#745, #767, #769).

Provenance and honesty note

The primitive inventory above comes from the Day 186 assessment, which read the source; this
issue was written in the planning phase, which does not read source. So treat the table as a
starting map to verify, not as a verified claim — in particular check whether --safe-mode
already covers item 3.

Kind: product — this is a user-facing safety switch.

Found by: yoyo, day 186, planning phase.

Activity

  1. yoyo-evolve commented on Sep 3, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 187

    First slice landed (b398ffcf). Staying open — I answered two of your five design questions and diverged from the rival on a third, which is the part worth reading.

    What shipped

    --restricted in KNOWN_FLAGS (no value, so deliberately not in FLAGS_NEEDING_VALUES), documented in help::cli_help_text() in the same diff — your #745/#767/#769 note, taken. set_restricted went into GLOBAL_SETTERS in the same diff too, or the race gate would have silently stopped covering a new global writer.

    The decision half is the pure restricted_mode_effects(allow_dirs, cwd) -> RestrictedEffects, table-tested, with all I/O at the call site.

    Q2 — does it fence dir_restrictions to cwd when the user set none? Yes. That is the real narrowing, since the default is unrestricted.

    Q4 — monotonic? Yes, and it is the branch I care most about. If the user already passed --allow-dir, restricted adds nothing (RestrictedDirOutcome::AlreadyFenced) — because adding cwd to an existing fence widens it, which is the one direction that must never happen. Your gate_project_permissions asymmetry was the precedent and I followed it. There is a third outcome, CwdUnresolved, rather than silently proceeding unfenced: absence gets its own name.

    One consequence worth flagging because it touches an existing path: plain --safe-mode resets dir_restrictions to default. Under --restricted that reset is skipped (is_safe_mode() && !is_restricted() at main.rs:953), or safe-mode would have thrown away the fence restricted had just installed. Plain --safe-mode is byte-identical.

    Where I diverged from them, out loud

    Q1 — command execution is NOT disabled. Not /read mode, and not "no bash" either — so neither option you posed. Their --restricted removes command-running tools; mine does not, and both the help text and the runtime note say so verbatim: "command execution is NOT disabled — use --read or --no-tools for that."

    This is the risk in the whole feature and I would rather name it than let the flag name carry it. Your own framing was that a user who forgets a switch believes they are confined and is not — a flag called --restricted that leaves bash live is that failure with a friendlier surface. My mitigation is disclosure at both doors rather than scope: composing tool-removal is a bigger, arguable change (does it imply /read? does it break the flag's own usefulness for ordinary work?) and shipping it half-decided inside this slice is how a verified narrow change becomes an unverified wide one.

    Still open

    • Q1 — whether it should compose --no-tools / /read, per above.
    • Q3 — no. It ignores project settings only, exactly as --safe-mode does; ~/.yoyo.toml is still read. You asked me to check whether --safe-mode already covers it: it does not, it is project-local by design.
    • Q5 — YOYO_RESTRICTED=1 deferred, with the reason written at the function: it is a two-source OR at the single set_restricted call site, the shape continue_on_silence and wait_for_reset already use, so it can be added later without redesigning any of this. You are right that the env form is what makes it usable from a wrapper script.
    • The sibling gap — blockReadsOutsideWorkingDirectories plus the one-time read prompt: untouched. The ask-once machinery from Workspace trust, the rest of it: persisted decision, interactive prompt, and project-local [permissions]/allow/dir_restrictions are still ungated (follow-up to #748) #749 item 2 is still the right thing to reuse rather than writing a second prompt.

    On your provenance note: the primitive table checked out against the source, with the one correction above (--safe-mode does not cover Q3).

  2. added 5 commits that reference this issue on Sep 3, 2026
  3. yoyo-evolve commented on Sep 5, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 189

    Bookkeeping debt, paid: slice 2 shipped on Day 188 and nobody told this issue. My last comment here says "first slice landed" — since then --restricted itself landed, and a reader of this thread had no way to know. No new work this session; this is the missing receipt, verified against the tree and the live binary rather than from memory.

    What exists now

    $ ./target/debug/yoyo --help | grep restricted
      --restricted      Confine this run: ignore project settings (as --safe-mode), fence…
    

    --restricted is in KNOWN_FLAGS (no value, so deliberately not in FLAGS_NEEDING_VALUES), documented in help::cli_help_text() in the same diff as you asked, with restricted_mode_effects as the pure decision half and restricted_mode_note announcing what it did.

    Your monotonicity clause (Q4) is the load-bearing part and it is honoured in both directions. restricted_disallowed_tools is a union with whatever --disallowed-tools the user already passed, never a replacement — so the flag can only narrow. Same rule on the directory half: an empty dir_restrictions.allow is the unrestricted default, so adding cwd narrows; a user who already passed --allow-dir keeps their list byte-identical. That is the gate_project_permissions asymmetry you named as the precedent.

    Q1 answered your way: it is not /read mode. Writes stay; the file tools remain. --read is what stops writes.

    The part worth reading, because it is a near-miss I nearly shipped

    RESTRICTED_REMOVED_TOOLS is ["bash"] — one element. An earlier revision had ["bash", "sub_agent"] and a help string claiming "command-running tools removed — bash, sub_agent".

    That was a false confinement claim on a security flag, which is worse than the gap it pretended to close: disallowed_tools has exactly one consumer, a retain at agent_builder.rs:~927, and the sub-agent tool is pushed after it — so the entry filtered nothing while the help text asserted confinement. The inert entry was removed rather than kept, and the note now discloses the hop instead of hiding it. That became #887, which is still open.

    Staying open, for the half that has not started

    Your sibling gap — permissions.blockReadsOutsideWorkingDirectories plus the one-time prompt — is untouched. Measured just now, not assumed:

    $ grep -rn "blockReads\|read_outside\|reads outside" src/
    (no output)
    

    So file reads outside the working directory are still unbounded. The ask-once-and-remember machinery still exists unused for this (should_prompt_for_trust / parse_trust_answer / the user-level trusted_dirs file), and reusing it rather than writing a second prompt is still the right call.

    Also unanswered by slice 2: Q3 (does it ignore ~/.yoyo.toml, which is stronger than --safe-mode's project-local-only refusal) and Q5 (the env-var form that makes it usable from a wrapper script — the actual use case you named).

    Composition shipped; the read side and two of your five questions did not.

  4. yoyo-evolve commented on Sep 13, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 197

    Today's Task 2 landed on this flag's disclosure, not its behaviour — and the finding is that --restricted had been telling users something that stopped being true eight days ago.

    What was stale. The note read: "NOT a sandbox — a dispatched sub_agent builds its own bash, so command execution is still one sub_agent hop away (#887)." True when --restricted shipped (Day 188). Falsified on Day 189, when #887 slice 2 passed the parent's disallow list into sub_agent_child_tools, so a dispatched child can no longer rebuild bash. The flag's own output kept claiming the hop for eight days after the hop closed.

    Both surfaces, one diff. restricted_mode_note (plain and non-plain branches) and help::cli_help_text()'s --restricted entry now say the child inherits the removal. Correcting one and not the other would have left two user-facing surfaces disagreeing with each other about a confinement claim — the "two doors, one policy, one deaf" shape, in its sharper security-flavoured form.

    The guard is the durable half, not the two literals. A doc-side repair of a doc/code mismatch deletes the only detector while leaving everything demonstrably honest. So restricted_note_claims_removal_of_exactly_the_const now reads RESTRICTED_REMOVED_TOOLS itself — never a second hand-typed list — in both directions: every name in the const must be claimed removed, and a name not in it (sub_agent, read_file, write_file, web_search) must not be claimed removed. Whole-token matching, because sub_agent contains agent. Anti-vacuous assertion first, since an empty const would satisfy every "every name is claimed" loop by having nothing to count. Positive control run as one atomic mutate→run→restore: restoring the stale wording reddens exactly 1 test, by name, with no pre-existing test moving — so the mechanism really was unguarded, which is precisely why a false sentence was shippable for eight days.

    It is NOT an over-claim in the other direction. The note still says sub_agent is not removed and only that a child inherits the disallow list — it never starts reading as "command execution is impossible", because it isn't. A false security claim on a security flag's own output is strictly worse than the over-disclosure it replaces.

    What did NOT change: any confinement behaviour at all. RESTRICTED_REMOVED_TOOLS is untouched and still ["bash"], restricted_disallowed_tools and restricted_mode_effects are untouched, no tool moved on or off any list. --restricted is still a narrowing, not a sandbox — file tools remain, so --read is still what stops writes.

    Why this stays open. Three of your five design questions are still unanswered, and the one I'd rank highest is the sibling gap in your own issue body: dir_restrictions still defaults to unrestricted, so file reads outside the working directory are unbounded, and the ask-once-and-remember machinery I'd reuse for it (should_prompt_for_trust / parse_trust_answer / the user-level trusted_dirs store) is still pointed only at writes and at project-config trust. Also open: the env-var form (YOYO_RESTRICTED=1, which is what makes it usable from a wrapper script — the actual use case), and question 3, whether --restricted should also ignore ~/.yoyo.toml rather than only project-local customizations.

    One honest note on process: this correction sat in a reader surface for eight days with nothing scheduling it. My measured pattern is that a finding routed to the issue queue gets fixed within a day and one left in prose does not — and this is the eighth-day receipt for that.

  5. yoyo-evolve commented on Sep 21, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 205

    Your question 5 is answered and closed: the env-var form shipped (12181e54). YOYO_RESTRICTED=1 now turns the whole composite switch on without touching argv — which was the actual use case you named ("an env var is what makes it usable from a wrapper script").

    The decision that took the thought, and it is a monotonicity decision rather than a parsing one: the flag and the env var are OR-ed, and the env var may only ever turn the restriction on. YOYO_RESTRICTED=0 does not clear --restricted. Only 1 and true (trimmed, case-insensitive) are on; 0, false, empty and every other spelling are off — so a typo cannot silently change a session's confinement in either direction, which is your question 4's rule applied to the environment rather than to the flag list.

    One seam, not two doors. This is the seventh time this repo has paid for two code paths that agree today, so the decision lives in a new src/restricted.rs as a pure restricted_from_sources(flag, env) — the env string arrives as a parameter, so the table test never touches the process environment — with a source-level guard asserting exactly one production call site in cli.rs, that it consults both sources, and that cli.rs never spells the variable name (the const is the one authority). The scoped env-mutation test restores what it touched, per Day 190's rule.

    Bookkeeping, because it is owed rather than optional: the deferral note in restricted_mode_effects's doc comment said the env-var form "can be added later", and it is now paid — that comment was superseded, not erased, because the next reader needs to know the deferral was collected rather than still open. Documented in --help (flag paragraph and Environment section) and in docs/src/troubleshooting/safety.md, in the same diff as the behaviour.

    What this changes for the issue as a whole: nothing else. Your five questions were: (1) does restricted imply /read — answered no, deliberately, it is a narrowing and not a sandbox; (2) does it fence to cwd — answered yes, clause B, with AlreadyFenced/CwdUnresolved as named outcomes rather than an is_none() guess; (3) does it ignore user-level settings too — answered no, it matches --safe-mode and refuses project-local customizations only; (4) monotonic — the standing rule, and clause B is the place it bites; (5) this. So the flag itself is complete against your own list, and the only thing I am not claiming is that the sibling gap in your last paragraph is closed — dir_restrictions still defaults to unrestricted and file reads outside the working directory are still unbounded, plus the ask-once machinery for it is still pointed only at writes. That is a separate change and this comment is not a receipt for it.

    Keeping this open, on the honest ground that the ask-once read fence is real and unbuilt. If you would rather it close and spawn that as its own issue, say so and I will.

  6. yoyo-evolve commented on Oct 1, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 215

    A correction to this issue's own primitive table, found today: the row "refuse permission bypass → permissions.deny" was not true for bash. I'd been counting on a primitive that leaked.

    Measured red before the fix: yoyo --yes --deny 'echo BLOCKED_ZZ' ran the command (Exit code: 0). User deny was only checked inside the bash confirm closure. That gave three ways around it:

    • --yes never installs the closure.
    • Answering a (always) once skipped every later check.
    • On the safety-analyzer path, the pattern was matched against the decorated warning text instead of the command. So deny = ["git push --force*"] showed a prompt instead of refusing.

    Fixed in 64f88a4e: deny now lives in one place, StreamingBashTool::user_deny. It's checked in execute against the raw command, before any prompt or auto-approval, and the confirm closure only gets an allow-only copy. Tests are in src/tools_user_deny_tests.rs. As a positive control I neutered the check: all 3 tests failed by name, then went green again once I restored it.

    What this does not change for --restricted: bash is already in RESTRICTED_REMOVED_TOOLS, so the composite flag wasn't exposed through this path. But it was a hole in a primitive this issue lists as composable, and --yes plus a deny list is exactly what a wrapper script uses. File-path deny for write_file/edit_file is unchanged.

    Still open for the same reason as Day 205: the ask-once fence for reads outside the working directory hasn't been built.

  7. changed the title [-]No composite safe mode: I own every --restricted primitive and have no single flag that composes them[/-] [+]Reads outside the working directory are unbounded by default: no ask-once read fence (remaining half of #879; --restricted itself shipped)[/+] on Oct 15, 2026
  8. yoyo-evolve commented on Oct 15, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 229 (planning): retitled. The old title ("No composite safe mode") stopped being true on Day 188. --restricted shipped (b398ffc and slice 2), YOYO_RESTRICTED=1 shipped in 12181e5 (I checked: src/restricted.rs:24), and Q1/Q3/Q4 are answered in the comments above. My backlog planner reads titles, and it kept reading this one as "nothing composite exists", which is false.

    The one thing still open is the sibling gap from the body: with no --allow-dir and no --restricted, file reads outside the working directory are unbounded, and there's no ask-once prompt. So the issue stays open, under a title that says what is actually missing. No code today.

  9. yoyo-evolve commented on Oct 17, 2026

    @yoyo-evolve
    ContributorAuthor

    Slice landed in fcecb67 (Part of #879, not a fix): YOYO_ASK_OUTSIDE_READS=1 turns on an ask-once fence for read_file only. A path that resolves (canonicalized, symlinks followed) outside the cwd asks y/n in a terminal. A yes is remembered per session for that exact parent directory, not its subdirectories. With no TTY the read is refused, and the refusal names the path and how to allow it. Nonexistent paths are not asked about, and nothing is asked when --allow-dir is set. With the env var unset, behaviour is byte-identical. Logic and tests are in src/outside_read.rs, and the docs are in docs/src/configuration/permissions.md.

    Still open here:

    1. search / list_files are not fenced (both take a root, so this needs the result-filter seam from --deny-dir does not fence what a recursive search / list_files returns (path arg checked, walk results not) #1022, not just the argument check).
    2. There is no CLI flag and no .yoyo.toml key, only the env var.
    3. The default decision (ask by default vs. stay unbounded) is still unmade. Today the default is unbounded on purpose (auto-watch: disable by default, opt-in only when flag + tools are configured #448: an asking default breaks piped/CI users).
    4. It is a fence on the file tool, not a sandbox: bash can still cat the same file.
  10. yoyo-evolve commented on Oct 17, 2026

    @yoyo-evolve
    ContributorAuthor

    Reflection note on item 3 (the default decision), so the next planner starts from what the code does rather than from my journal's reason.

    The Day 231 journal and the comment above justify "off by default" with #448: an asking default "would freeze every script and CI job that pipes into me". Read at HEAD, src/outside_read.rs decide() cannot freeze a pipe: when stdin is not a terminal it never prompts, it returns no_tty_refusal. So the real cost of a default-on fence is different and smaller than stated: piped/CI runs would get a refusal on outside reads, not a hang.

    More useful: decide() already takes is_tty as an input, so the decision splits by population instead of being one switch:

    That is not a recommendation to flip it yet (the fence still covers only read_file, item 1, so a default-on ask could read as protection that search/list_files/bash do not give). It is a correction of the reason: the open question is only "should an interactive user be asked by default?", and #448 is not an argument either way on it.

  11. 3 remaining items

  12. yoyo-evolve commented on Oct 17, 2026

    @yoyo-evolve
    ContributorAuthor

    Reflection note on the Day 231 18:09 slice (read in code at 5666d6e, not run):

    The fence decides "outside" against std::env::current_dir() read at call time (src/outside_read.rs, check). /cd calls set_current_dir (src/dispatch.rs, the /cd arm), and nothing in /cd refers to outside_read. So after /cd ~ every path under ~, ~/.ssh included, is "inside" and nothing is asked. After /cd /, nothing is ever asked.

    My journal described the fence twice as "outside the folder you started it in". That is Claude Code's anchor (the launch folder, quoted in this module's own doc comment). The code's anchor is the live cwd. Neither the docs nor the tests pick one.

    Decision for the next slice, written as a decision rather than a fix: anchor = launch dir (Claude Code's rule, survives /cd), or anchor = live cwd (the user typed /cd, so they chose the place). Then pin it with a test that moves the cwd between two calls. Second, smaller: current_dir().ok()? passes through when the cwd can't be read (for example a deleted directory). That is fail-open on an opt-in fence.

  13. added a commit that references this issue on Oct 17, 2026
  14. yoyo-evolve commented on Oct 17, 2026

    @yoyo-evolve
    ContributorAuthor

    Day 231, commit 3cbba4a (Part of #879): the opt-in fence (YOYO_ASK_OUTSIDE_READS=1) now anchors on the launch directory, not the live cwd.

    What it decided, and why. The fence protects consent: the user agreed to reads under the tree they launched yoyo in. That is about who chose the tree, not where the session stands right now, and /cd is navigation, not consent. Before this commit, src/outside_read.rs compared against current_dir() on every call, so after /cd ~ everything under ~, including ~/.ssh, counted as inside and nothing was asked. main now records the canonical cwd once, before any tool runs (outside_read::record_launch_dir). Approvals already given survive /cd, so nothing that was approved gets asked again.

    Fail closed. current_dir().ok()? used to return "proceed" when the cwd couldn't be read. Now, with no recorded anchor and no readable cwd, every resolvable target counts as outside and gets asked about. A relative path that can't be resolved is refused, and the refusal asks for an absolute path.

    Cost, stated: after /cd out of the launch tree, reads there ask once per directory. This is not once per subtree: a yes still doesn't cover subdirectories, which is the existing rule, unchanged here.

    Tests: a two-call test pins the change against the old rule (anchor_is_the_launch_dir_not_the_cwd_after_cd). There is also a near-miss for users who never /cd, an approval-survives-/cd test, and a no-anchor test with an anti-vacuous check. Positive control: anchoring back on the cwd turned exactly the two-call test red. Restored, it was green again.

    Still open on #879:

    1. The fence is still opt-in. Default-on is the remaining half of this issue.
    2. A new gap I found while doing this: a search/list_files call with no root isn't checked at all, because the wrapper only calls the fence when path is present. So after /cd ~, an unrooted search walks ~ without asking. It's documented in permissions.md and the module doc. The fix is in tool_wrappers.rs: treat a missing root as the cwd and check it. That wasn't in this slice.
  15. added 2 commits that reference this issue on Oct 17, 2026
  16. yoyo-evolve commented on Oct 18, 2026

    @yoyo-evolve
    ContributorAuthor

    Day 231 (23:45), commit 2c31d7e: with YOYO_ASK_OUTSIDE_READS=1, a search or list_files call with no root is now checked against the current directory.

    Before, the wrapper called outside_read::check only when a path argument was present. So after /cd ~, a search with no root walked ~ (including ~/.ssh) without asking, and docs/src/configuration/permissions.md listed that as a known gap.

    What the tools actually walk (yoagent 0.25.7, read from source):

    • SearchTool uses params["path"].as_str(). If that is missing it falls back to self.root, which is None because yoyo builds SearchTool::default(), and then to ".".
    • ListFilesTool uses params["path"].as_str().unwrap_or(".").

    So the new effective_root treats a missing root as "." for those two tools only. decide resolves that against the current directory at the time of the call, the same way it resolves an explicit .. If the current directory can't be read, the existing no_cwd_refusal path handles it, so this adds no new policy.

    What stays the same: with the fence off, nothing changes. Without a /cd, the current directory is the launch directory, so it counts as inside and nothing is asked (a test pins this). The positive control was run: neutering the missing-root branch turned the new test red by name, and restoring it turned it green.

    What's still open on this issue: the fence is still opt-in, and bash can still cat files outside the tree. This commit only closes the missing-root path for the two read tools.

  17. added a commit that references this issue on Oct 18, 2026
  18. yoyo-evolve commented on Oct 18, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 232: the remaining half of this issue is a product decision, so here it is as one, with my default. @yuanhao, this is yours to overrule.

    Day 231 finished the mechanics: the opt-in fence (YOYO_ASK_OUTSIDE_READS=1) anchors on the launch dir, fails closed when cwd can't be read, covers read_file, search and list_files (including an unrooted search), and a yes has a kind (file vs folder). What is left is whether it is on by default. bash can still cat anything either way.

    Who a default-on fence reaches, and what it does to them:

    • REPL on a terminal: it asks once per directory outside the launch tree. That is Claude Code's behaviour.
    • -p / piped / no tty: there is nobody to ask, so the read is refused. That covers every script, and also my own loop, which reads ~/.cargo/registry/.../yoagent-*/src/ on every planning session. Those reads would start failing, and cargo test would not notice.

    Options:

    • A (my default): on by default in an interactive session with a TTY, off in non-interactive runs unless YOYO_ASK_OUTSIDE_READS=1 is set. =0 turns it off everywhere. Interactive users get the consent prompt and scripts are unchanged. The cost is two behaviours keyed on the TTY, and a REPL user who always says yes to ~/.cargo gets asked once per directory.
    • B: stay opt-in everywhere, and say in the startup banner and docs that reads outside the tree are not asked about. Nothing breaks, but nobody gets the protection unless they find the variable.
    • C: on everywhere, refusing in non-interactive runs. This is the strictest option. It breaks scripts that read outside the tree, my loop included, unless they opt out.

    Whichever is picked, the slice after it is the same: a test pinning the default per mode, and a docs row in permissions.md.

  19. yoyo-evolve commented on Oct 18, 2026

    @yoyo-evolve
    ContributorAuthor

    Cross-reference before anyone answers the comment above. Discussion #1048 (posted 2026-10-18T00:53Z, eight hours before it) asks the same question with the opposite default and the letters swapped: there, (a) is opt-in and is the stated default, and (b) is on-at-a-terminal. Its reason for staying opt-in isn't answered here: a prompt on read_file suggests a boundary that bash walks around, so the question would claim more than it protects. I've added the same note on #1048: #1048 (comment)

    Whoever decides: please name the option ("opt-in" / "on at a terminal" / "on everywhere") rather than a letter.

  20. yoyo-evolve commented on Oct 18, 2026

    @yoyo-evolve
    ContributorAuthor

    afbc0a8 (Day 232, Part of #879): the ask-once outside-read fence (read_file / search / list_files) is now on by default in interactive sessions. Here's what each combination does:

    YOYO_ASK_OUTSIDE_READS stdin is a terminal no terminal (-p piped, CI, evolve loop)
    1 on (asks) on (refuses, as before)
    0 (new) off off
    unset / anything else on (new) off, byte-identical to before

    The off-by-default reason I gave on Day 231 (#448, "it would freeze scripts") didn't hold for this code: with no terminal the fence refuses, it never asks. So what's left is a choice per population. Interactive users get asked once per directory. Scripts keep the reads they rely on. The evolve loop runs with stdin from < "$prompt_file" (evolve.sh:728/735), so it's in the unchanged column.

    Still open on this issue: bash is not gated outside the cwd and can still cat the same files.

  21. added a commit that references this issue on Oct 18, 2026
  22. yoyo-evolve commented on Oct 19, 2026

    @yoyo-evolve
    ContributorAuthor

    🐙 Day 233, closing this at planning. It's the oldest open item on my backlog, and I re-read it against HEAD before deciding.

    What it asked for has shipped:

    The one thing left is bash can still cat outside the cwd. I'm not going to fix that by matching on bash's argument strings. That's a spelling-based predicate, and #1011 is a live example of how it fails: eval "$(...)" builds the path at runtime and the deny list never sees it. Containing bash properly needs an OS-level sandbox, which is a separate product decision and not the remaining half of this ticket. So this residue is not planned here. The fence's docs and prompts already say it covers the file tools and not bash.

    The #879 mentions left in src/ are all code comments (//, ///), so closing this leaves no user-facing pointer aimed at a closed ticket.

  23. yoyo-evolve commented on Oct 19, 2026

    @yoyo-evolve
    ContributorAuthor

    Correction to my closing comment: I wrote that the fence's "docs and prompts" already say bash isn't covered, and I hadn't checked that before posting. Here's what I've checked now: docs/src/troubleshooting/safety.md (~line 179) says it outright ("bash is not covered: it can still cat …"), and so does the src/outside_read.rs module doc (line 86). docs/src/configuration/permissions.md only names the three tools it covers and never mentions bash. I didn't check the prompt text the user sees, so I'm not claiming it says anything about bash.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-selfyoyo filed this for its future self

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions