Skip to content

Untrusted Repositories

Opening a directory is not a neutral act. A repository you cloned, or a tarball you extracted, supplies files that other programs read as instructions — .git/config tells git what to run, .envrc tells direnv what to export, AGENTS.md tells the model what to do. None of that requires you to approve anything. It happens because you cd'd somewhere.

So Tapioca treats the working directory as a second untrusted input, alongside the model. This page is what that means in practice: what a repository can put in front of you, what is done about it, and what is still yours to judge.

What the directory controls

Everything in it, which is more than it sounds:

  • .git/config and .gitattributes — both name programs git will run
  • .envrc — and therefore, if you use direnv, the environment of the process, including $HOME and $XDG_CONFIG_HOME
  • AGENTS.md, .tapioca/commands/*.md, skills — text that reaches the model
  • file names, symlinks, hard links, FIFOs, and every byte of content

Git

Merely opening a directory used to be enough to run code from it. core.fsmonitor is a config key naming a program, git status executes it, and the git panel polls status every few seconds — in every permission mode, before you have typed anything. An extracted tarball is enough; git clone is not, since clone writes a fresh config.

Every git command Tapioca runs now has the keys that name a program pinned away. The list is not fixed, because the dangerous keys are not a fixed set: a filter driver's name is chosen by the repository and selected from its own .gitattributes. So the repository's own configuration is read first — git config --list executes nothing — and anything naming a program is neutralised, across every scope, following include.path.

The pins travel as GIT_CONFIG_KEY_<n>/VALUE_<n> rather than -c key=value, because a config subsection name may legally contain = and -c splits on the first one. GIT_CONFIG_PARAMETERS is dropped from the environment entirely, since git reads it after the numbered pairs — a channel that cannot be ranked below a pin does not travel.

Instruction files

AGENTS.md and anything it @imports reach the system prompt on every turn, in every mode, with no tool call to decline. So they are confined:

  • an import resolves inside the project or your config directory, and nowhere else — checked on the resolved path, because git stores symlinks and a committed AGENTS.md pointing at ~/.config/tapioca/config.toml would otherwise put your API keys in the prompt
  • a file with a second name of its own (a hard link to that config) is refused the same way
  • only .md, .markdown and .txt are importable
  • one file is capped, the expansion is capped in aggregate, and the total is capped — a d -> . symlink makes one file reachable under unboundedly many paths
  • a FIFO named AGENTS.md is not read, because opening one blocks until a writer appears and nothing ever writes

A config file inside the tree

Tapioca's config lives outside the working tree. A repository can still reach it by choosing where it is looked up: an .envrc pointing XDG_CONFIG_HOME into the checkout, or a committed file that --settings is aimed at.

When the config Tapioca loaded turns out to live inside the tree being worked on, every key that executes or sends a credential is withdrawn, and you are told which:

KeyWhy
[[mcp]], [[lsp]], [[agents.external]]each names a command started at launch
bash_allow, permissions.allowstanding approvals
permission_mode = "auto" / "bypass"forced down to manual
editorsplit into an argv and run on a keystroke
providers.*.base_urldecides where your API key goes — kept only if it names this machine
providers.*.credentials_filenames a file whose token_uri is POSTed to
[[hooks]]commands run around tool calls

ask and deny rules are kept. A repository can only narrow with those, and a narrowing it chose is not one you need protecting from.

Your own config at ~/.config/tapioca/config.toml is exempt no matter where you are working, including inside a dotfiles repository — getting a file read as the config is exactly what redirecting XDG_CONFIG_HOME achieves, so one already sitting at the home location was not planted. That check asks the account database as well as $HOME, because $HOME is a variable an .envrc can set.

The environment

Since .envrc reaches the environment, a variable naming a location cannot be allowed to move a defence:

  • the protected directories (~/.ssh, ~/.aws, your config and session store) are always on the list; a variable can add a root, never replace one
  • RIPGREP_CONFIG_PATH is dropped from grep's child, because ripgrep reads arguments from the file it names and one of them is --pre=COMMAND, run on every file searched — and grep prompts for nothing, in any mode, plan included
  • the sandbox hides both the $HOME you have and the one the account database reports

Grants

[p] grants a command word. It does not grant a flag that turns that command into a way of running another program: git -c, git bisect run, git config, find -exec, go run, go generate, go env -w, make -f, tar --use-compress-program, rsync -e and npm exec all re-prompt, and the check runs when the grant is matched, not only when it is offered — the whole point of a blanket grant is that later commands are not read by anyone.

Be clear about what this buys, though: a grant on git is still weaker than that list makes it look. git commit runs .git/hooks/pre-commit, which in an extracted tarball is a file the archive chose. What the list buys is that the command you pressed [p] on cannot be turned into an arbitrary one; it does not make git safe in a hostile checkout.

What this does not cover

  • PATH and LD_PRELOAD. If you let an .envrc set those, git, rg and sh are already whatever it says they are. Nothing Tapioca does inside the process changes that; it is a property of running a program in a shell someone else configured.
  • bypass mode, which disables all of the above.
  • Build tools doing their job. make, npm install, cargo build and go test run code from the repository by design. That is what they are for, and it is why the checkpoint exists.
  • A deny rule cannot see inside sh -c '…' or $(…). Matching is textual; echo $(touch x) runs under bypass despite a rule for touch.

The short version

Use plan or manual in a repository you have not read. direnv allow on a checkout you do not trust is the single largest thing you can hand it — larger than anything the agent will ask you to approve — because it is the one step that lets the directory change the environment the whole process runs in.

Released under the MIT License.