Mount Exclusions¶
Nyia Keeper hides sensitive files (API keys, certificates, cloud credentials) from the container using Docker overlay mounts — the agent sees a read-only placeholder, while the rest of your project stays fully visible. The built-in patterns below are a starting point, not the finish line: they catch the common cases, but only you know which files are sensitive in your project. Review what Nyia will hide, and add your own private files before your first launch.
Exclusion hides the working file, not old commits. A secret that was ever committed is still recoverable from git history (
git show,git log -p). To keep the container from reading old commits too, use Git History Cutoff.
Default Patterns¶
The built-in exclusion list covers several categories of sensitive content:
Cloud providers -- .aws/, .gcloud/, .azure/, .digitalocean/, service account JSON files, AWS credential files, Azure publish settings, and more.
SSH and certificates -- .ssh/, .gnupg/, private keys (*.pem, *.key, *.ppk), certificates (*.crt, *.csr), and known-hosts files.
Kubernetes and orchestration -- .kube/, .minikube/, .helm/, kubeconfig files, and configs for K3s, Rancher, OpenShift, and Istio/Linkerd/Consul service meshes.
Infrastructure as code -- .terraform/, .pulumi/, Terraform state and variable files (*.tfstate, *.tfvars), and CloudFormation parameter files.
CI/CD -- .jenkins/, .circleci/, .buildkite/, GitHub Actions secrets, GitLab CI local variables, and ArgoCD/Tekton config files.
Configuration management -- .ansible/, .chef/, .puppet/, .vagrant/, Ansible vault files, Chef validation keys, and SaltStack configs.
Package managers -- .npm/, .yarn/, .cargo/, .gem/, .m2/, .gradle/, registry auth files (.npmrc, .pypirc), and language-specific credential stores. Package manager cache directories get writable tmpfs mounts so the container can use them without leaking data to the host.
Environment and secrets files -- .env, .env.*, *secret*, *password*, credentials.json, and similar patterns.
Databases, message queues, and monitoring -- connection configs for PostgreSQL, MySQL, Redis, Kafka, RabbitMQ, plus Datadog, New Relic, Prometheus, and Grafana configs.
Shallow patterns -- directories like vault/, secrets/, credentials/, certs/, and backups/ are checked only at the project root and one level deep to avoid false positives inside dependency trees.
The full list is defined in lib/mount-exclusions.sh (functions get_exclusion_patterns(), get_exclusion_dirs(), get_shallow_exclusion_dirs(), and get_exclusion_path_patterns()).
Note: the defaults exclude only genuinely sensitive files. Some ops-config filenames that merely reference secrets rather than contain them —
nginx.conf,config.yaml,.travis.yml,go.sum,ansible.cfg,docker-compose.override.yml— are not excluded by default, so you can edit them normally; the certs/keys they reference stay excluded via*.pem/*.key/*.crt. If your project keeps a secret in one of these, re-add it with a custom exclusion (below).
Custom Exclusions¶
Add your own patterns to .nyiakeeper/exclusions.conf at the root of your project. Edit this file on the host (your editor), when no session is running: inside the container it is a placeholder (see Control files).
# Match anywhere in the tree (no '/' in the pattern)
*.secret
my-local-config.json
my-secrets/
# Root-anchored (the pattern contains a '/')
config/database.yml
docs/internal/
# Root-anchored with a leading '/'
/top-level-notes.txt
/src/sensitive.key
# Anywhere, at any depth including the root ('**/' prefix)
**/private-logs/
**/config/database.yml
Every launch creates this file (with a commented header) when it is missing. It is safe to commit (the generated .gitignore rule re-includes it) or to leave untracked.
Lockdown mode generates an exclude-everything config that you then whitelist:
nyia exclusions lockdown # Scan project, generate exclude-everything config
nyia exclusions lockdown --force # Overwrite existing config (backs up first)
nyia exclusions lockdown --workspace # Apply to all RW workspace repos
Lockdown writes one root-anchored line per top-level entry (/src/, /customer-data.csv). Run it again when you add top-level files or directories: a new entry is not covered.
Pattern Matching Rules¶
The forms below work both as an exclusion and, with a ! prefix, as an override:
| Pattern | Kind | Matches |
|---|---|---|
.env |
file, anywhere | a file named .env at any depth, the root included |
secrets/ |
dir, anywhere | a directory named secrets at any depth |
*.backup, key?.txt, [ab].pem |
file glob, anywhere | by name, at any depth |
config/database.yml |
file, root-anchored | only <root>/config/database.yml |
docs/internal/ |
dir, root-anchored | only <root>/docs/internal/ |
/notes.txt |
file, root-anchored | only <root>/notes.txt |
/src/ |
dir, root-anchored | only <root>/src/ |
**/token.txt |
file, anywhere | token.txt at any depth, the root included |
**/private-logs/ |
dir, anywhere | private-logs/ at any depth, the root included |
**/config/database.yml |
file, anywhere | config/database.yml at the root and under any directory |
**/a/b/ |
dir, anywhere | a/b/ at the root and under any directory |
Key rules:
- No
/(or only a trailing/): matches by name, anywhere. - Contains
/(not just trailing): anchored to the project root. - Leading
/: root-anchored. **/prefix: anywhere, the root included.- Trailing
/: directories only. Without it, a pattern matches files only:secrets(no slash) does not hide a directory namedsecrets(unlike gitignore). #at line start: comment. Leading and trailing spaces and a Windows line ending are ignored.
Wildcards: *, ? and [...] work. * can cross / (unlike gitignore). For most configs this makes no difference.
Depth: the scan goes 5 levels deep. A file inside 5 or more nested directories (a/b/c/d/e/x.key) is not found by a file pattern. Exclude one of its parent directories instead.
Quotes: as in earlier versions, a matching pair of quotes is removed ("my notes.txt" is the name my notes.txt). So a name that really contains a pair of quotes or apostrophes, such as it's a 'test'.key, cannot be written as is: it becomes its a test.key and the file stays visible. Escape each quote with a backslash (it\'s a \'test\'.key) or use a glob (it?s*.key). A single, unpaired apostrophe (it's.key) is kept as written.
Dependency directories (node_modules, vendor, site-packages, __pycache__, .venv, venv, target) are not searched inside. A directory pattern that names one hides it like any other directory: node_modules/ and **/vendor/ hide every such directory the scan reaches (5 levels deep), and /vendor/ only the one at the root. What is inside a dependency directory is mostly not checked: a file pattern never hides a file in it (node_modules/pkg/.env stays visible), and a name-only directory pattern (private-logs/) never hides a directory in it. Path forms (/node_modules/pkg/) still work there. To keep a secret there out of the container, hide the dependency directory itself. With the node flavor, Nyia already replaces <root>/node_modules with an empty container-only directory.
Deep scan (opt-in). To see what is inside the dependency directories, run nyia exclusions list --deep: it searches them once, with every pattern, and lists what it finds there under its own heading ("Found inside dependency directories"). It writes nothing, and it does not change what launches hide. To make every launch search them too, set this on the host, in ~/.config/nyiakeeper/config/nyia.conf:
Only this global file counts: the project's .nyiakeeper/nyia.conf is ignored for this setting, because a session can write it. The file is read as text, never run. With the setting on, a launch prints one line saying so. It costs time on every fresh scan: on a test project with about 50,000 files in node_modules, a fresh scan took 1.3 to 2 seconds longer (GNU find; more with a long project path). The cache records the scan mode, so a cache made in the other mode is not reused: the launch scans again.
Built-in patterns that contain a / (.gem/credentials, group_vars/*/vault, ...) match that path at any depth, the root included.
Case: on macOS and on Windows (WSL2) projects, matching ignores case, for exclusions and ! overrides alike. On Linux it is case-sensitive.
Override Patterns¶
To keep visible a file or directory that is hidden by default, prefix the pattern with !:
# Keep .aws/ visible (overrides the built-in exclusion)
!.aws/
# Keep a specific key file visible, anywhere
!deploy-key.pem
# Root-anchored: only <root>/config/credentials.json
!config/credentials.json
# Anywhere, the root included
!**/fixtures/test.key
Overrides follow the same rules as exclusions (table above).
A file inside an excluded directory cannot be re-included¶
As in gitignore, a ! line cannot make a file visible when one of its parent directories is excluded. The directory is replaced as a whole, so its files never reach the container. nyia exclusions list (and a launch with --verbose) prints a warning for such a line:
'!**/secrets.nix' cannot apply to secrets/secrets.nix: its parent secrets/ is excluded; use '!secrets/' to re-include that directory
Example: with secrets/secrets.nix and docs/secrets-setup.md in a project, this config makes docs/secrets-setup.md visible but not secrets/secrets.nix, because secrets/ is a built-in excluded directory:
Add the directory override to make secrets/secrets.nix visible too:
Keep the !**/secrets.nix line: secrets.nix also matches the built-in file pattern *secret*. With !secrets/, the directory is no longer hidden as a whole, so its files are checked one by one: a file that matches a built-in or user pattern (another *secret* file, a *.key) stays hidden, and every other file in secrets/ becomes visible. Check the result with nyia exclusions list.
A ! line committed in a shared exclusions.conf applies to everyone who pulls it. Review changes to that file like code.
Built-in security exclusions (non-overridable)¶
A small set of raw secret files is always stubbed inside the container and cannot be
force-included with !:
.nyiakeeper/private/creds/env(and the legacy.nyiakeeper/creds/env)
These hold credentials that Nyia parses on the host and forwards into the container as
allow-listed environment variables — no in-container code reads the raw file — so stubbing it is
safe defense-in-depth and prevents the raw credential file from being read inside the container
(for example by an automated reviewer). The stub applies on every launch: even with
--no-exclusions / ENABLE_MOUNT_EXCLUSIONS=false, on cache hits, and regardless of
exclusions.conf (a ! re-include does not un-stub it). Files that Nyia genuinely reads
in-container — .nyiakeeper/private/network-allow.conf (egress firewall) and
.nyiakeeper/cli-runner-access/id-runner (sidecar SSH key) — are deliberately not stubbed.
(Plan 314.)
Control files¶
exclusions.conf and the cache decide what the next launch hides. In a project's own session (session, reviewer or worker, exclusions on or off), no container can change them:
.excluded-files.cacheis the cache from the run dir, mounted read-only at this path. Nyia's container startup reads it to keep the placeholders out ofgit status.exclusions.confand.cache-metaare replaced by a placeholder that says the file is hidden. An assistant cannot read or edit your exclusions; editexclusions.confon the host, when no session is running..nyiakeeper/is mounted onto itself, so a container cannot rename it to get around the mounts above. Other files in.nyiakeeper/(plans, context) stay writable.- A
!line cannot remove these protections.
Every launch creates .nyiakeeper/, exclusions.conf (with its commented header), the cache and .cache-meta when they are missing, even with exclusions disabled. The conf is safe to commit or to leave untracked. The cache and .cache-meta are covered by the generated /.nyiakeeper/* ignore rule (in .gitignore, or in .git/info/exclude with hide_footprint=local; see Configuration).
A directory the host cannot read (for example after chmod 000, or a root-owned database volume) is hidden from the container as a whole: Nyia cannot check what is inside it. The launch says how many such directories it hid (--verbose names them). A ! line does not re-include one: make it readable on the host.
A launch is refused (with a message that says what to fix) when, on the host:
.nyiakeeperor one of the three control files is a symbolic link, or not a regular file or directory. This applies even with--disable-exclusions. Replace the link on the host.- a control file has a second hard link. Check its content, then remove the other name (the message gives the
find ... -samefilecommand). - the project directory or
.nyiakeeper/is read-only and a control file is missing. Nyia must create the files before a container starts. Runchmod u+won it. - a file name contains a newline, another control character,
,or:directly at the project root. The same name deeper in the tree is hidden by replacing its parent directory. Rename or delete it. - the scan could not read part of the tree and found no unreadable directory that explains it (for example, a directory changed during the scan). Run the launch again.
Workspace Mode¶
In workspace mode, each repository can have its own exclusions.conf:
repo1/.nyiakeeper/exclusions.conf # Applied to repo1 mount
repo2/.nyiakeeper/exclusions.conf # Applied to repo2 mount
workspace/.nyiakeeper/exclusions.conf # Applied to main workspace
The same built-in patterns apply to every repository independently. Both read-only (ro) and read-write (rw) mounts receive exclusions. When one mounted directory sits inside another, its exclusions also apply on the path through the outer mount.
Every rw repository gets the same control-file protection as the workspace root (its conf and .cache-meta are placeholders; repositories have no cache). When an rw repository has no .nyiakeeper/, the launch creates it with exclusions.conf (commented header) and .cache-meta, before any container starts, even with exclusions disabled. Why: a repository's own conf is applied, so a session could otherwise create one for the next launch. An rw repository is one the assistant works in, so the standard conf belongs there, as at the root. A repository that is not for assistant work should be listed read-only (ro) in workspace.conf: Nyia writes nothing into an ro repository, unless it lies inside an rw mount (for example inside the workspace root). There the assistant can write to it through the outer mount, so it gets the same conf and protection (unless the outer mount hides it, or you can neither write nor change the permissions of that directory, so no session can either). When the launch creates a repository's .nyiakeeper/, it also adds /.nyiakeeper/ to that repository's .git/info/exclude (local, never committed), so git status stays clean and git add -A && git commit works in the container. It does not run git for this, skips a repository whose .git is a file (a worktree or submodule), and writes nothing through a link. The launch is refused when a repository's .nyiakeeper is a symbolic link or not a directory, when a control file in it has a second hard link, or when an rw repository's directory is not writable for you (make it writable, or list it ro).
CLI Options¶
Disable for one session¶
Pass the --disable-exclusions flag to bypass all exclusions for a single session:
The container will see every file in your project, including secrets. Use this only when you explicitly need the assistant to access excluded content.
Disable via environment variable¶
Set the ENABLE_MOUNT_EXCLUSIONS environment variable to false:
This has the same effect as --disable-exclusions.
Troubleshooting¶
Why is my file hidden?¶
Run with --verbose to see every file and directory that gets excluded:
The output will show lines like:
Excluding file: path/to/my-file.pem
Excluding directory: .aws
Override: keeping file visible: deploy-key.pem
Look for your file in the exclusion output to understand which pattern matched it. The verbose launch also prints the warning for a ! line that cannot apply (see above).
How do I check what is excluded?¶
nyia exclusions list # What the next launch hides (per repo in a workspace), and '!' lines that cannot apply
nyia exclusions list --recreate # Rebuild the cache now with the launch's own scan, then list
nyia exclusions list --deep # Also search inside node_modules, vendor, .venv ... (once, writes nothing)
nyia exclusions patterns # Every pattern, with its anchoring
nyia exclusions status # Whether exclusions are enabled, and the config files
nyia exclusions list runs the same scan as a launch and never changes the cache.
A file I need is being excluded¶
- Check which pattern matched it (use
--verboseornyia exclusions list). - Add an override to
.nyiakeeper/exclusions.confon the host: If the file is inside an excluded directory, override the directory (!dir/), see above. - Start a new session: the file is visible in the container.
The cache¶
A launch saves its scan in Nyia's host-private run dir, outside every mounted directory: ~/.config/nyiakeeper/run/project-<sha256 of the project path>/excluded-files.cache by default, with its cache-meta beside it. This is the only copy a launch trusts. No container can reach it, including one that mounts this project as a workspace repository or inside a larger project.
.nyiakeeper/.excluded-files.cache and .nyiakeeper/.cache-meta are copies with the same content. The first is where the container sees the cache (read-only). Nyia never decides anything from them, so editing them changes nothing. The first launch after an upgrade scans again.
The next launch reuses the cache only when all of these hold:
- it is less than 60 seconds old (
NYIA_CACHE_TTL, in seconds, set in the host environment); exclusions.confhas not changed;- it was made in the same scan mode (deep scan on or off);
- every path it lists still exists.
Otherwise the launch scans again. A project's exclusions.conf is still read from the project: a session with write access to a member or nested project's .nyiakeeper/ can change that conf for the next launch. Within those 60 seconds, a new secret file is not seen. When a valid cache exists, nyia exclusions list says so and how long it stays valid. Run nyia exclusions list --recreate to rebuild it now.
Security Model¶
Mount exclusions protect the filesystem inside the container by replacing excluded files with placeholder stubs. Understanding what they protect — and what they don't — is important for managing secrets effectively.
What exclusions protect¶
Files that are not in git and should not be: .env files listed in .gitignore, local credential files, private configs that were never committed. For these files, the protection is effective:
| Layer | What it does | Protects against |
|---|---|---|
| Placeholder stubs | Replaces real files with empty stubs in the container | AI reading file content |
| Control files | The cache lives outside the project and is mounted read-only, the conf and .cache-meta are stubs, .nyiakeeper/ cannot be renamed |
A session changing what the next launch hides |
| Host launch checks | Refuses a symlinked, hard-linked or oddly named control file; host writes under .nyiakeeper/ (launch, nyia plans migrate) refuse a planted link, with one exception (see Known limits) |
A session planting files for the host to trust or overwrite |
| Skip-worktree | Hides placeholders from git status and git add |
Accidental staging of stubs |
| Pre-commit guard | The image runs it before the repository's own pre-commit hook (nothing is written to .git/hooks). Blocks commits containing placeholder marker text |
Accidental commit of stubs (advisory, see below) |
| System prompt | Instructs AI not to access excluded paths or their git history | AI curiosity |
The AI never sees the real content of a hidden file.
The pre-commit guard is advisory: it stops accidents, not a hostile session. Inside the container, git commit --no-verify or git -c core.hooksPath=... gets around it. The real guarantee is on the host: the launch checks and the mounts, which nothing inside the container can change.
What exclusions don't protect¶
Files that were ever committed to git history. The .git/ directory is mounted read-write inside the container (required for branch operations, commits, pushes, etc.), except its config, hooks and commondir, which are read-only. If a sensitive file was committed even once, the AI can access the original content through many git commands:
git show HEAD:.env # Shows committed content
git checkout -- .env # Restores real file, replacing placeholder
git restore .env # Same as above
git log -p -- .env # Shows full diff history with content
git reset --hard # Restores all files from last commit
This is a fundamental limitation of any system that mounts .git/ read-write. Blocking individual git commands is impractical — git has too many ways to surface file content (checkout, restore, reset, merge, rebase, cherry-pick, stash pop, etc.), and blocking them would break normal git workflow.
A committed exclusions.conf and branch switches¶
Inside the container, .nyiakeeper/exclusions.conf is a read-only placeholder, so no session can change what the next launch hides. If your team commits that file and another branch changes it, then inside the container git checkout <branch> (or a git pull that touches the file) stops with:
error: Your local changes to the following files would be overwritten by checkout:
.nyiakeeper/exclusions.conf
git stash does not help ("No local changes to save"). This is expected: to switch to such a branch, exit the session and switch that branch on the host, then start a new session. Branches that do not change the file switch normally.
User responsibility for git history¶
If a secret was committed to git, you have several options:
- Rewrite history: Tools like
git filter-repoorBFG Repo-Cleanercan permanently remove sensitive files from all commits. - Git-level encryption: Some tools encrypt file content at the git blob level using clean/smudge filters. With such a setup, the working tree has decrypted files (which exclusions protect), but the
.git/object store has encrypted blobs — sogit show HEAD:.envreturns encrypted data that is useless to the AI. This provides full coverage when combined with mount exclusions. Tool choice is up to you. - Rotate secrets: If a secret was exposed in git history, consider it compromised and rotate it regardless of other mitigations.
Best practice¶
Avoid committing sensitive files to git in the first place. Use .gitignore alongside mount exclusions — this way, exclusions protect the working-tree copy and there is no git history to worry about.
Known limits¶
- Editing a hidden file on the host while a session runs. Most editors,
git pull,git checkoutandmvsave a file by replacing it (a rename). A running session then sees the new content of that file: a rotated secret, an edited.env, orexclusions.conf(which that session can then also edit for the next launch). Edit secrets andexclusions.confonly when no session is running, or restart the sessions afterwards. - The project-root
.gitignoreupkeep no longer follows a link a session planted: a linked.gitignore, or a linked.git/infodirectory, is refused and left untouched. The one exception is the fallback writer used only when Nyia'sfootprint-policy.shlibrary is missing from the install. The legacy.nyarlathotia/migration and the git-history cutoff re-clone no longer do. The migration runs on every launch, because a session can create.nyarlathotia/. It refuses a linked directory, skips links inside it, copies only the files.nyiakeeper/does not already have, and does not keep executable bits; a file others cannot read is copied private. The git-history cutoff shallow is no longer in the project at all (see below). Runscripts/migrate-to-nyiakeeper.sh(manual, it still usescp -a) only with no session running. - Docker resolves each mount source when the container starts, and follows a link. So the git-history cutoff shallow is kept in Nyia's host-private run dir, which no container can see, and every launch re-checks its mount sources right before
docker run. A source must still exist and not be a link. A source inside another mounted directory (the.nyiakeeper/self-bind, the cache, a workspace repository inside the root) must have no link below that directory. Otherwise the launch is refused. The re-check narrows the window but does not close it: a session that swaps a source in the fraction of a second between the check and Docker reading it can still win. The re-check cannot see directories that only another running container mounts: for example, a project inside a workspace root that a session is using. Do not launch a project while a session of an enclosing workspace runs. - The cache is trusted for up to 60 seconds. A change to the file tree in that time (for example a hidden directory moved by a session) can be missed by a launch that reuses the cache. Every cached path must still exist, which catches deletions but not every change.
- A committed
exclusions.confcan stopgit checkoutinside the container (see above). - Read-only projects do not launch until Nyia can create its control files (see Control files).
- A project path that contains
,cannot be launched (the read-only cache mount refuses it). - Names with paired quotes need escaping in
exclusions.conf(see Pattern Matching Rules). - Speed. Each excluded directory adds work for every file checked. A project with a very large number of excluded directories (for example, planted by a session) launches slowly.
- Workspace repositories: a tracked file that is hidden in a workspace repository shows as modified in that repository's
git status(the hook refuses to commit it).nyia exclusions listshows each mounted directory at its own mount only, not on the path through an outer mount.
What happens on upgrade¶
- The first launch rebuilds the cache (format v7).
- If your
exclusions.confhas!lines, that launch prints a one-time notice:!**/overrides used to be ignored, and now take effect. Check them withnyia exclusions list: they may make files visible that were hidden before. - A directory pattern that names a dependency directory (
node_modules/,**/vendor/) now hides it; before, it hid nothing. Eachrwworkspace repository (and eachroone inside anrwmount) gets a.nyiakeeper/exclusions.confwhen it has none. - Patterns with a leading
/,**/patterns at the project root,**/a/bpatterns, lockdown configs and the built-in patterns that contain a/now hide what they say. Before, they hid less (lockdown configs hid nothing). - Restart running sessions: a session started before the upgrade does not have the new control-file protections. Update the image too: the git protection of workspace repositories is part of the image.