I asked a coding agent to explain a failed terraform plan. It did. The context-pack preview also listed terraform.tfstate and a .env beside the module. Did the model need either file to explain a missing variable? No. Did the process have permission to open both? Yes. That is the failure I now refuse to skip. I have not filed it as a production incident, and I am not claiming a vendor leaked anything.
A DEV discussion this week argued that models improved while the software around them did not. I am not relitigating that post. I am asking a narrower question. If the indexer got better at walking a tree, did your allowlist shrink, or did you hand it a bigger directory?
Two boundaries, not one
People collapse these into an AI policy slogan, and then they test the wrong layer.
The first boundary is local and boring. Which paths can the agent uid open before a single token is spent? If open succeeds, you already lost, even when the weights run on a box you racked yourself.
The second boundary is egress. A hosted free server is not a cheaper synonym for a local model. Bytes leave the laptop. Self-hosting can shrink that second boundary. It does nothing for the first.
So which one are you actually proving when you say secrets never reach a model?
laptop
agentfix uid -- test -r --> src/app.js allow
agentfix uid -- test -r --> .env deny
hosted free server egress, canaries only
What stays out of the index
I keep the denylist dull. Clever patterns miss the file the walker opens first.
Credential basenames come first: .env, .env.local, .netrc, .npmrc, .pypirc, id_rsa, id_ed25519, and credentials. Cloud and cluster admin files sit next to them: anything named kubeconfig, plus the usual AWS and gcloud config trees if you were careless enough to mount a home directory. State and dumps follow: terraform.tfstate, terraform.tfstate.backup, SQL dumps, and packet captures.
VCS metadata is the sneaky one. A .git/config remote with a userinfo field is a token that does not look like a token. Would I paste that list into a chat and ask the model to be careful? No. The decision is a file-mode check. A sentence in a system prompt is not a lock.
Step 1. Pin the tools and the manifest
This whole fixture is an unexecuted template. I did not run it in CI this week. Expected lines below are assertions you should see if you run it, not a log I captured.
Pin the interpreter before you trust the exit code. I wrote the gate for Bash 5.2, Git 2.40 or newer, and GNU findutils 4.9. On macOS, take those from Homebrew. BSD find will not honor the same flags, and a green run on the wrong find is not evidence.
# readset.deny — exact basename, one per line
.env
.env.local
.netrc
.npmrc
.pypirc
id_rsa
id_ed25519
credentials
kubeconfig
terraform.tfstate
terraform.tfstate.backup
A basename of credentials will also match a harmless test file with that name. That false positive is intentional. Rename the test file. Do not delete the rule because it annoyed a unit test.
Step 2. Build the positive fixture
Use a scratch directory on a lab VM. Never point the first run at your real home folder, and do not create this uid on a shared workstation.
mkdir -p /tmp/readset-fixture/src
printf 'AWS_SECRET_ACCESS_KEY=canary-not-real\n' > /tmp/readset-fixture/.env
printf 'console.log(1)\n' > /tmp/readset-fixture/src/app.js
chmod 600 /tmp/readset-fixture/.env
sudo useradd --system --create-home --shell /usr/sbin/nologin agentfix || true
sudo chown root:root /tmp/readset-fixture/.env
The positive case is the secret file. If agentfix can test -r it, the gate must exit 1. A policy line that says ignore secrets does not count. Why would it? The walker does not ask permission in English.
Step 3. Keep a negative fixture readable
src/app.js is the negative case. The gate must not fail merely because source code exists. If the denylist is so wide that the agent cannot read the file you asked it to review, someone will chmod 777 the tree and call it unblocked. I have seen that workaround on other gates. I do not want this one to teach it.
Ask one question before you add a path to the allow side. If I replaced the contents with the string REDACTED, could the agent still answer the question I typed? If yes, the file stays denied. If no, allow it, and add it to the negative fixture so a later rule cannot swallow it quietly.
Step 4. Run the gate as the agent uid
#!/usr/bin/env bash
# Unexecuted template. Assertions are intended, not captured.
set -euo pipefail
root="${1:?workspace root}"
agent_user="${2:?agent uid name}"
deny="${3:?path to readset.deny}"
fail=0
[[ -d "${root}" ]] || { printf 'missing root: %s\n' "${root}" >&2; exit 2; }
id "${agent_user}" >/dev/null
while IFS= read -r base || [[ -n "${base}" ]]; do
case "${base}" in
''|\#*) continue ;;
esac
while IFS= read -r -d '' path; do
if sudo -u "${agent_user}" test -r "${path}"; then
printf 'DENY readable by %s: %s\n' "${agent_user}" "${path}" >&2
fail=1
else
printf 'ok denied: %s\n' "${path}"
fi
done < <(find "${root}" -xdev -name "${base}" -print0)
done < "${deny}"
safe="${root}/src/app.js"
if [[ ! -f "${safe}" ]]; then
printf 'negative fixture missing: %s\n' "${safe}" >&2
exit 2
fi
if ! sudo -u "${agent_user}" test -r "${safe}"; then
printf 'SAFE path not readable, denylist too wide: %s\n' "${safe}" >&2
fail=1
fi
exit "${fail}"
Intended evidence, once you actually run it: exit 1 and a DENY readable line when .env is mode 644; ok denied plus exit 0 when .env is mode 600 and owned by someone other than agentfix, while src/app.js stays readable. If you cannot create that uid, stop. A check that runs as you will pass for every file you can already read. That green bar is worse than no bar.
find -xdev will not cross filesystems. A secret mounted elsewhere is invisible to this gate. If the agent can see that mount, pass it as its own root, or drop -xdev and accept the slower walk. Either way, write the choice down. A silent skip is not a deny.
Step 5. Decide where the bytes are allowed to go
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The gate above does not know which model you call next. That is the point. I still want a cheap place to rehearse the deny path, because a script that never runs is a comment with a shebang.
For this draft, MonkeyCode is described as offering free model access and a free server option. I am not stating a token quota, a hardware shape, a region, or an expiry. Those figures move, and I have not re-checked a primary page while writing. Read the project docs on the day you rely on them.
Here is the split I would not blur.
| Place you run the rehearsal | Proves the uid cannot read the denylist? | Proves bytes stayed on your network? | Fit |
|---|---|---|---|
| Local agent uid, no model call | Yes, if sudo -u is real |
Yes, nothing left | Always run this first |
| Model server you already operate | No, unless you also ran the gate | Only if you configured it that way | After the gate is red or green for real |
| Hosted free server | No | No | Canary trees only |
Use the hosted option only after the positive fixture contains a canary string, not a live key. A free server answers whether the workflow can run. It does not answer whether the data may leave. If your team needs a contract before any workspace byte leaves the laptop, skip the hosted path and point the same gate at a model you already run.
If you want a non-production checkout to prove the gate fails closed, that free model access is enough to rehearse the deny path. It is not a reason to mount a production tree.
Prevent, detect, recover
| Phase | Control | Evidence that counts |
|---|---|---|
| Prevent | Dedicated agent uid, no home mount, denylist checked with test -r
|
Denylisted paths print ok denied
|
| Detect | CI runs the gate on the workspace before any indexer starts | Non-zero exit and the path, not a model summary of the path |
| Recover | Rotate anything that was readable, then fix the mode | New canary present, old canary absent from agent logs |
Recovery is rotation, not a request that the model forget. If a hosted server already received the file, treat it as disclosed to that operator and follow your own vendor process. I will not invent a deletion promise here.
Logs are part of the same boundary. A gate that prints the secret while proving the secret was readable has just copied it into CI output. Print the path. Never print the file. Would you scroll that log into a chat to ask why CI failed? Then the log is the next thing to deny.
Who should not use this
Skip the hosted rehearsal if the checkout holds customer data, production state, or anything under a residency clause. The local gate is still worth running. A zero invoice does not make a server a sandbox.
Skip the whole approach if you cannot create a separate uid. Also skip it as a replacement for history scanning. This fixture sees the current tree and one uid. It will not see a token committed last March and deleted from HEAD. Pair it with a history scanner you already trust. I am not crowning one.
Do not use the gate as proof that a product is safe. It proves one invariant on one machine: this uid cannot read these basenames. That is all. It also will not catch a secret pasted into a source comment, a screenshot, or a prompt you typed by hand. Those need a different fixture.
What this draft is not
I did not execute the template against MonkeyCode. I did not measure latency, token spend, or a vulnerability. There is no CVE in this post, and there is no benchmark. If you run the script, keep the output. That output is the evidence. The draft is only the procedure.
One invariant belongs in CI. The agent uid cannot read the denylist. One layer should enforce it. Filesystem permissions, before the indexer starts. The model is a reader. It is not the lock.
Which path in your tree is still mode 644 only because the agent might need it?
Top comments (0)