janusfs
a filesystem for AI agents

Allow. Deny.
And the third answer: masked

Agents get a filtered mirror of your project — real code, real structure. Secrets are redacted byte-for-byte, right where they read; forbidden files fail closed.

Install Read the source
$ curl -fsSL https://raw.githubusercontent.com/sarathsp06/janusfs/main/install.sh | sh

Linux · one static binary · all install methods → · optional --net=none kills the network too

.env masked view same size
# .env — what the agent reads
APP_URL=https://api.acme.com
API_KEY=sk_live_************
DB_URL=postgres://app:********@db.acme
LOG_LEVEL=debug
Real URLs and structure pass through. Only the secret bytes are starred — same path, same byte length.
allowedpassthrough
maskedsecret spans starred
hiddenEACCES

why it exists

Every sandbox asks one question: allow, or deny. Both answers are wrong.

A file with a secret inside it has no good binary answer. Deny it and the agent that needed the file's shape breaks; allow it and the secret lands in the model's context. That dilemma is why agents either stall or over-share — and it's the gap JanusFS fills, one layer down at the filesystem.

answer 1 · deny

Block the file

A deny rule, Landlock, an .aiexclude. The secret is safe — but the agent that needed that config value, that fixture, that file's shape now fails, loops, or hallucinates around the hole.

✗ the agent breaks

answer 2 · allow

Hand it over

Let it read the file so it can work. Now the secret sits in the context window, the transcript, the provider's logs — and one prompt injection turns cat .env into curl attacker.com.

✗ the secret leaks

janusfs · the third answer

Show a masked face

The file lists at its real size and reads back with only the secret spans replaced by *, byte-for-byte. The agent gets the shape and the safe content; the secret never enters. Truly forbidden files still fail closed.

✓ the agent works, the secret stays

the three faces

What each decision does to a read.

Every file still lists with its real size — masking preserves byte length, so getattr and offsets are identical whether or not a span is redacted. One .janusfs.yml drives all three.

statelisting / sizereadwrite
allowedyes / realpassthroughpassthrough
maskedyes / real*-redactedEACCES
hiddenyes / realEACCESEACCES

Hierarchical & fail-closed. Files further down the tree override shallower ones, and allow can re-include a hidden file — but never one under a directory that resolves to Hidden. janusfs explain <path> traces every contributing rule.

.janusfs.yml — one file, three concepts
version: 1

hide:
  - "*.pem"
  - id_rsa*
  - .aws/credentials
  - node_modules/

allow:
  - .aws/known_public_config

mask:
  - paths: ["*.env*"]
    patterns: [env-value]
  - paths: ["**/*"]
    patterns: [aws-key, github-token, jwt, private-key]
  - paths: ["config/**/*.yaml"]
    patterns: [generic-secret, db-uri]
  - paths: ["secrets/*"]   # whole-file mask

01 — rules

Compile .janusfs.yml globs + pattern library.

02 — engine

Resolve a Decision behind an atomic, immutable snapshot.

03 — provider

Serve redacted bytes from a RAM cache — never disk.

04 — mount

Translate the Decision into a kernel answer over FUSE.

use cases

Point it at a directory. Run anything against the mirror.

Because it's a filesystem, whatever reads files works unchanged — the agent, its shell, git, the build, an MCP server. You don't wire JanusFS into your tool; you run your tool inside JanusFS.

Run a coding agent on a secret-laden repo

aider, Claude Code, Codex, Cursor read your source and reason about it, but .env values and API tokens read back as **** and private keys fail closed. Every child process inherits the view.

janusfs exec -- aider

Cut the exfiltration path

Masking keeps secret bytes out of the agent; --net=none keeps whatever it did read from leaving the box — loopback only, no route out, kernel-enforced.

janusfs exec --net=none -- claude

A persistent masked mirror for any tool

Hand a long-lived mountpoint to an MCP server, a code indexer, a REPL, or a teammate's script. The daemon owns it and remounts after a reboot.

janusfs daemon --background
janusfs mount .
cd "$(janusfs path .)"

Audit before you trust it

See exactly what an agent would read — every likely-secret still Allowed, and why any single path resolves the way it does — before you run anything against it.

janusfs check --secrets
janusfs explain .env

enforcement

Own the process tree, not one channel

janusfs exec confines the entire subprocess tree at once: its read_file tool, its Bash tool, git, a build, a subprocess — all inherit the filtered view, with no per-tool wiring and no way to opt out.

platform

Non-Linux is refused, not faked

mount and exec error out on macOS and Windows rather than pretend an advisory mount is a boundary. On a Mac, run the agent in a Linux container/VM started with --device /dev/fuse --cap-add SYS_ADMIN.

one caveat, warned loudly

git stages masked bytes

Because .git/ passes through to the real object store, git add on a masked file stages the **** bytes. check and exec both report every masked file git would stage.

where it fits

A complement to your sandbox, not a competitor.

Sandboxes and containers decide allow/deny and protect the machine; JanusFS masks the bytes. Run janusfs exec inside them.

approachsecrets inside useful files?survives iterations?zero config-per-repo?
.gitignore / .aiexcludewhole-file onlyyesper-repo
Harness sandboxallow or denyyesyes
Docker bind mounthands over raw fileyesimage setup
One-shot scrubbingfrozen snapshotnoyes
Per-tool read filterseasily bypassedper-tool disciplineper-tool
JanusFSper-span, byte-preservingFS boundary, per-readmachine-wide floor

why not .gitignore?

Whole-file only. A config with one secret line is all-or-nothing; JanusFS redacts the line and keeps the rest of the file readable.

why not a sandbox?

It guards the machine, not the model's context. Files it lets through still flow verbatim into the agent and its provider.

why not scrub once?

A frozen copy drifts the moment the agent edits a file. JanusFS decides per read, against the live tree, so it never goes stale.

get running

One command in, masked in a minute.

JanusFS runs on Linux only. Install the FUSE runtime, drop in the binary, seed a policy, then run your agent inside the boundary — or hand a persistent mount to the daemon.

1

Install the FUSE runtime

sudo apt-get install -y fuse3   # Debian/Ubuntu
# sudo dnf install -y fuse3     # RHEL/Fedora
2

Seed a policy & preview

cd my-project
janusfs init
janusfs check --secrets
janusfs explain .env
3

Run the agent inside it

janusfs exec -- aider
janusfs exec --net=none -- aider
4

Or a persistent mount

janusfs daemon --background
janusfs mount ~/proj
cd "$(janusfs path ~/proj)"
janusfs umount ~/proj

On macOS or Windows, run all of this inside a Linux container or VM (Docker Desktop, Colima, OrbStack, a devcontainer) started with --device /dev/fuse --cap-add SYS_ADMIN — both mount and exec refuse to run natively off Linux. Prefer Go? go install github.com/sarathsp06/janusfs/cmd/janusfs@latest builds from source.

stand at the doorway

One command, and the secrets never enter.

In Roman myth, Janus is the two-faced god of doorways. JanusFS stands between your code and any untrusted agent, deciding which face of each file is safe to show — allowed, masked, or hidden.

$ curl -fsSL https://raw.githubusercontent.com/sarathsp06/janusfs/main/install.sh | sh

Linux amd64 & arm64 · all install methods → · prebuilt releases →