cd /news/ai-agents/build-a-homedir-deny-fixture-before-… · home topics ai-agents article
[ARTICLE · art-134441] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Build a Homedir Deny Fixture Before an Agent Reads `.netrc`

A developer built a pair of local test fixtures and a fail-closed gate script, assert_agent_root.py, that refuses to let a coding agent's file tools open homedir credential files such as ~/.netrc, ~/.pgpass, and ~/.docker/config.json. The walkthrough reproduces an incident in which an agent opened at ~/work followed a curl --netrc flag into ~/.netrc and quoted an internal API credential line back into the chat, which the developer attributes to a trust-boundary miss rather than prompt injection. The gate denies agent roots that resolve inside $HOME or contain deny-listed basenames, and uses fake canary tokens as a test oracle for downstream leaks.

by read8 min views1 publishedSep 19, 2026

Last Tuesday a coding agent “helped” with a flaky checkout script. The repo was tiny. The workspace was not.

Someone had opened the agent at ~/work. checkout.sh called curl --netrc. The agent did what file tools do: it followed the comment, opened ~/.netrc, and quoted a machine api.internal line back into the chat. No prompt injection. No jailbreak theater. A trust-boundary miss.

If your agent can read a credential file, the model is not the interesting part. The path is. Would you paste that file into Slack? Then why can a tool loop open it?

This is a lab walkthrough, not a vulnerability report. I did not find a CVE. I built two fixtures and a gate that fails closed when the agent root can see homedir secrets. Treat the commands as a local regression, not evidence that any hosted product leaked production credentials.

Here is the event order I keep reproducing in a throwaway directory. It is boring. That is the point.

~/work because “that is where the repos live.”~/work/shop/checkout.sh contains curl --netrc -f "$API_URL/health". checkout.sh, then follows the flag to ~/.netrc. machine, login, and password lines. Those lines also land in agent traces, session exports, and whatever log shipper you forgot. The trust boundary is not “the model is aligned.” The boundary is “which realpaths the file tool may open.” Mix $HOME into that set and you are doing vibe ops. You are not doing engineering.

I only care about three files in this fixture. Expand later if you must. Do not start with a 40-row spreadsheet and zero failing tests.

Asset Trust boundary What the agent should never receive Failure mode
~/.netrc Homedir credential store, not the git tree machine /login /password stanzas Chat, traces, and fine-tunes learn internal API basic auth
~/.pgpass libpq client secret file host:port:db:user:password Integration-test “help” dumps a live DB password
~/.docker/config.json Registry auth cache auths.*.auth (base64user:pass ) Agent “debugs a pull” and reprints a registry token

Secondary assets I deny by name in the same gate: id_rsa, id_ed25519, .git-credentials, kube user.token files, and credentials under .aws. I still do not treat a filename deny-list as a sandbox. It is a CI tripwire. Shell tools can walk $HOME unless you block that too.

What about logs? Same rule. If the agent cannot open .netrc, it also cannot open a support zip that already copied .netrc. Redact at the workspace edge. Do not pray at the prompt.

Python 3.12. No third-party packages. Unexecuted against your laptop until you run it; expected evidence is the gate’s exit code, not a screenshot of a vendor UI.

Layout:

agent-root-lab/
  tools/assert_agent_root.py
  fixtures/positive/src/checkout.sh
  fixtures/negative/.netrc
  fixtures/negative/.pgpass
  fixtures/negative/.docker/config.json
  fixtures/negative/src/checkout.sh

Positive checkout.sh talks to a public health endpoint and does not mention --netrc. Negative checkout.sh does. That is deliberate. The gate must fail on the credential files even if you later rewrite the script.

Negative canaries. Fake on purpose:

machine api.internal.example
login ci-bot
password canary-netrc-token-NOTREAL
127.0.0.1:5432:shop:shop:canary-pgpass-NOTREAL
{
  "auths": {
    "ghcr.example.invalid": {
      "auth": "Y2FuYXJ5OmNhbmFyeS1kb2NrZXItYXV0aC1OT1RSRUFM"
    }
  }
}

If a later export, RAG chunk, or chat transcript contains canary-netrc-token-NOTREAL, you have a leak in the pipeline. That string is the test oracle. Keep it ugly so grep never collides with a real password.

tools/assert_agent_root.py is the artifact. It refuses $HOME, /, and any agent root that contains deny-listed basenames. It also refuses a root whose resolved path is inside $HOME unless you pass --i-mean-it — which CI must not pass.

#!/usr/bin/env python3
"""Fail closed if an agent workspace can see homedir credential files.

Lab fixture for Python 3.12. Not a sandbox. Not a vulnerability report.
"""
from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

DENY_NAMES = {
    ".netrc",
    ".pgpass",
    ".npmrc",
    ".git-credentials",
    "id_rsa",
    "id_ed25519",
    "credentials",  # .aws/credentials
    "kubeconfig",
}

DENY_TAIL = {
    (".docker", "config.json"),
    (".aws", "credentials"),
    (".kube", "config"),
}

def fail(msg: str) -> int:
    print(f"FAIL: {msg}", file=sys.stderr)
    return 1

def iter_files(root: Path):
    for p in root.rglob("*"):
        if p.is_file() and not p.is_symlink():
            yield p

def denied(path: Path) -> str | None:
    if path.name in DENY_NAMES:
        return f"basename {path.name}"
    parts = path.parts
    for tail in DENY_TAIL:
        if parts[-len(tail) :] == tail:
            return "/".join(tail)
    return None

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("root", type=Path)
    parser.add_argument("--i-mean-it", action="store_true")
    args = parser.parse_args()

    root = args.root.expanduser().resolve()
    home = Path.home().resolve()

    if not root.is_dir():
        return fail(f"{root} is not a directory")
    if root in {Path("/"), home}:
        return fail(f"agent root must not be {root}")
    if home == root or home in root.parents:
        return fail(f"{root} sits inside $HOME")
    if root == home or home in root.parents:
        pass
    if str(root).startswith(str(home) + os.sep) and not args.i_mean_it:
        return fail(f"{root} is under $HOME; isolate the workspace")

    hits = []
    for path in iter_files(root):
        why = denied(path)
        if why:
            hits.append(f"{path} ({why})")

    if hits:
        print("FAIL: credential paths inside agent root:", file=sys.stderr)
        for h in hits:
            print(f"  - {h}", file=sys.stderr)
        return 1

    print(f"PASS: {root} has no deny-listed credential files")
    return 0

if __name__ == "__main__":
    sys.exit(main())

Expected evidence, labeled because I am not running this against your tree:

python3 tools/assert_agent_root.py fixtures/negative; echo exit:$?

python3 tools/assert_agent_root.py fixtures/positive; echo exit:$?

python3 tools/assert_agent_root.py "$HOME"; echo exit:$?

If the negative case prints PASS, the gate is wrong. Fix the gate. Do not “prompt the model to be careful.”

Layer Action Who owns it
Prevent Agent root is a dedicated directory with an allowlist of src/ ,tests/ , and lockfiles. Homedir is not mounted. Shell tool working directory is that root. Platform / agent harness
Prevent CI runs assert_agent_root.py on the directory you pass to the agent, not on the git repo after a hopeful.gitignore CI
Detect Grep session exports and tool traces for canaries ( canary-netrc-token-NOTREAL ) Security / SRE
Detect Deny-list is necessary but weak; also block read /exec outside the root at the tool broker Agent runtime
Recover Rotate the canary’s real counterpart if a production file ever matched. Treat chat history as compromised. Wipe the session store. Incident

.gitignore is not a trust boundary. Agents do not only see committed files. They see the working tree, and they see whatever you bind-mounted “for convenience.”

Say it in one list so nobody has to infer it from vibes.

.netrc, .pgpass, .npmrc, .git-credentials ~/.docker/config.json auths values, even when they look like base64 sludgekubeconfig user tokensrepr(), or Spring application-local.yml Comments in Makefiles that say “uses netrc” are fine. The file those comments point at is not. If the agent needs to fix checkout.sh, give it a fake NETRC=/workspace/fixtures/netrc.example with canaries, not your laptop file.

The temptation is to enable a coding agent on the same box you use for git push and docker login. Free compute makes that temptation worse. You skip isolation because the experiment “should only take ten minutes.”

I use an isolated workspace for this lab: source plus fixtures, no homedir overlay, no production kube context. MonkeyCode’s free model access and free server option are relevant here as a disposable box for that workspace — not as a reason to rsync $HOME. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

If the server still mounts your developer dotfiles, the price being zero does not change the threat model. Isolation is the feature. The model is just the consumer on the far side of a path allowlist.

A public clone of the gate plus the two fixtures is enough to review the idea. You do not need my account, and you do not need a vendor UI, to fail the negative case.

Filename deny-lists miss secrets with polite names. local.settings.json, helm/values-prod.yaml, and terraform.tfvars will sail through unless you add content scanning. This script does not open files. That is intentional. It is a path invariant, not a secret scanner.

Symlinks: I skipped them on purpose so a malicious tree cannot make the walker follow ../../.netrc into an infinite mess. Your harness still must refuse readlink outside the root. Otherwise the gate is theater.

Do not use this approach if:

read is worthless next to cat ~/.netrc. Also do not paste the negative fixture into a shared eval set that ships to a third-party trainer unless those canaries are the only secrets in the tree. Eval corpora become training corpora. You already knew that. Act like it.

Which invariant belongs in CI, and which layer should enforce it?

CI should fail if the directory you will pass to an agent contains deny-listed credential paths, or if that directory resolves under $HOME. The tool broker should enforce the same realpath allowlist at runtime, including shell cwd. The model should see neither .netrc nor a log that already copied it.

If you only do the prompt, you are calling it engineering. You are not doing it.

── more in #ai-agents 4 stories · sorted by recency
── more on @curl 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/build-a-homedir-deny…] indexed:0 read:8min 2026-09-19 ·