Package Exports
- postgres-vfs
- postgres-vfs/commands
Readme
A PostgreSQL-backed virtual filesystem for AI agents. Agents run cat, ls, grep, find, mkdir, rm, and 70+ other Unix commands against a persistent, multi-tenant workspace and all data lives in Postgres.
Built on just-bash a TypeScript bash interpreter, with a custom IFileSystem that compiles every filesystem call into SQL. Inspired by ChromaFs but it can also read-write and is Postgres-native.


Why do we need postgres-vfs?
Modern LLMs are fluent in shell. They've ingested enormous amounts of grep, find, cat, sed, and awk during training, which makes a bash tool over a filesystem a surprisingly high-leverage interface for non-coding agents too (email, sales, support, docs). grep returns exact matches. Directories map to real data hierarchies. Agents load only the context they need, when they need it. See this note on how swapping custom tools for bash + filesystem cut cost and improved output in production (part of the broader MCP-vs-CLI_with_fs conversation).
But a real filesystem is a poor production substrate for agents:
- Sandboxing is slow and expensive. Per-container spin-up takes seconds, and isolated disks add up fast at agent scale.
- No built-in multi-tenancy. Isolating agents means managing Unix users, chroots, or per-session containers.
grep -ris linear. Byte-scanning thousands of files doesn't scale as workspaces grow.- State is ephemeral in a sandbox Files die with the sandbox unless you bolt on volumes, and backup/restore is per-box.
- No indexed search in a regular filesystem It doesnt have FTS, trigram, no vectors. Every agent reinvents retrieval on top of raw files.
postgres-vfscollapses bash + filesystem onto a single Postgres table. Agents keep their nativegrep/cat/findworkflow. Isolation is row-level. Search is indexed (FTS + trigram + optional pgvector). State persists across sessions. A new workspace spins up in milliseconds.
Table of Contents
1. Getting Started
Install
bun add postgres-vfs # recommended
# or
npm install postgres-vfsThe package is ESM-only and ships with TypeScript declarations. Requires Node.js >= 18 or Bun >= 1.0.
Quick usage
import { createClient, createPostgresVfsTool } from "postgres-vfs";
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
const sql = createClient("postgres://postgres:postgres@localhost:5432/postgres_vfs");
const { tools, sandbox } = await createPostgresVfsTool({
sql,
tenantId: "my-agent",
});
// Pre-load files for the agent
await sandbox.writeFiles([
{ path: "/docs/readme.md", content: "# Hello World" },
]);
// Agent can now use bash, readFile, writeFile tools
const result = streamText({
model: anthropic("claude-sonnet-4.5"),
tools,
maxSteps: 10,
prompt: "List all files in /docs and summarize them",
});
for await (const part of result.textStream) {
process.stdout.write(part);
}Custom shell commands (search, tag, recent, etc.) are available as a subpath export:
import { createCustomCommands } from "postgres-vfs/commands";Prerequisites
Quick start (Docker)
# Start PostgreSQL with pgvector
docker run -d --name pgvfs \
-e POSTGRES_DB=postgres_vfs \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
pgvector/pgvector:pg17
# Clone, install, configure, migrate
git clone https://github.com/sumedhkhodke/postgres-vfs.git
cd postgres-vfs
bun install
echo "DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres_vfs" > .env
bun run migrateIf everything worked, you should see:
Migrating database...
Migration complete.Manual setup
# Create the database and install extensions
createdb postgres_vfs
psql -d postgres_vfs -c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# Configure, install, migrate
echo "DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres_vfs" > .env
bun install
bun run migrate
# Launch the Web UI
bun run uiYou should see:
postgres-vfs UI running at http://localhost:4321Open http://localhost:4321 in your browser.
2. Features
- 75+ bash commands.
grep,sed,awk,jq,find,sort,wc, pipes, redirections, variables, loops. All backed by Postgres. - Three-tier search. Full-text (tsvector), fuzzy/regex (pg_trgm), and optional semantic similarity (pgvector).
- Auto-embedding pipeline. Files are embedded on write and queries use vector similarity when
OPENAI_API_KEYis set; silently degrades without it. - Multi-tenant isolation. Row-Level Security guarantees each agent only sees its own files.
- bash-tool integration. Drop-in compatibility with bash-tool.
- Web UI. Three-column file manager for browsing, editing, searching, and tagging.
- In-memory caching. Path tree, directory listings, and stat data cached on init for sub-millisecond lookups.
- Persistent across sessions. Files survive restarts; agent state carries over.
3. Architecture
Four layers between the agent and the database. Each layer knows only the interface above it.
3.1 Layered stack

Figure 1: Four layers between the agent and the database.
3.2 Request flow
The agent sees three tools: bash({ command }), readFile({ path }), and writeFile({ path, content }).
When the agent calls bash({ command: "grep -rn TODO /projects" }), the call walks down the stack, turns into SQL at the bottom, and the result walks back up.
At a high level, a natural-language prompt maps to code entities across four layers (natural language → agent interface → code entities → persistence):

Figure 2: Prompt → agent interface → code entities → persistence.
Concretely, here's what grep -rn TODO /projects compiles to at the bottom of the stack, i.e. the SQL that actually runs against Postgres:
-- IFileSystem.readdir('/projects') becomes:
SELECT path FROM vfs_files
WHERE tenant_id = 'agent-123'
AND path LIKE '/projects/%'
AND path NOT LIKE '/projects/%/%';
-- IFileSystem.readFile('/projects/src/server.ts') becomes:
SELECT content FROM vfs_files
WHERE tenant_id = 'agent-123'
AND path = '/projects/src/server.ts';Postgres returns the rows, just-bash's grep runs the regex on the content and emits matching lines to stdout, and the agent sees the result of its tool call. The agent never sees SQL. just-bash never sees Postgres. Each layer is replaceable.
3.3 PostgresFs and IFileSystem
IFileSystem is a ~20-method TypeScript contract from just-bash. Implement it and you can run bash against any backing store. The default implementation uses a JS Map; ours uses Postgres.
interface IFileSystem {
readFile(path): Promise<string>;
writeFile(path, content): Promise<void>;
readdir(path): Promise<string[]>;
stat(path): Promise<FsStat>;
mkdir(path): Promise<void>;
rm(path): Promise<void>;
exists(path): Promise<boolean>;
// + cp, mv, symlink, chmod, link, utimes, etc.
}just-bash thinks it's talking to a filesystem. It's actually talking to a database.
3.4 Search engine
The search command fans out to FTS, trigram, and (optionally) pgvector in parallel, then combines their scores with a weighted formula.

3.5 Two-stage grep
grep -r across thousands of files completes in milliseconds because it runs in two stages:
- Coarse filter in Postgres:
WHERE content ~ 'pattern'uses the pg_trgm GIN index to drop non-matching files instantly. - Fine filter in JS: precise line-by-line regex on the candidate set, with line numbers.
3.6 Caching
On init(), PostgresFs loads the complete path tree into memory:
Set<string>of all paths →exists()is O(1).Map<string, string[]>of directory children →readdir()is O(1).Map<string, CachedStat>of file metadata →stat()is O(1).
Writes invalidate the cache surgically (only affected paths), so reads stay fast during active use.
3.7 Embeddings pipeline
When OPENAI_API_KEY is set, file writes trigger a fire-and-forget embedFile() call that stores a vector in the embedding column. Search queries are embedded too, enabling semantic matches in hybridSearch(). When the key isn't set, embedding calls return null, search falls back to FTS + trigram, and no API calls are made.
3.8 VFS vs native filesystem
postgres-vfs and the agent's native filesystem are separate systems that coexist. /projects/foo.ts in the VFS is a row in Postgres, not a file on disk.
| Agent's native filesystem | postgres-vfs | |
|---|---|---|
| Storage | Your actual disk | PostgreSQL vfs_files table |
| Access | Built-in tools (Read, Edit, Bash) | bash-tool tools (bash, readFile, writeFile) |
| Scope | Everything on your machine | Isolated per tenant |
| Search | Sequential file scan | FTS index + trigram index + optional vector |
| Security | Agent can read/write anything allowed | Agent can only see its own tenant's files |
| Persistence | Until you delete the file | Until you delete the row |
4. Database
Everything lives in two tables: vfs_files for regular files and directories, vfs_symlinks for symlinks. Isolation, search, and backup are all just Postgres features.
4.1 Schema
CREATE TABLE vfs_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id TEXT NOT NULL DEFAULT 'default',
path TEXT NOT NULL,
is_dir BOOLEAN NOT NULL DEFAULT FALSE,
content TEXT,
mode INTEGER NOT NULL DEFAULT 33188, -- 0o100644
summary TEXT,
tags TEXT[] DEFAULT '{}',
size INTEGER GENERATED ALWAYS AS (octet_length(coalesce(content, ''))) STORED,
content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', coalesce(content, ''))) STORED,
embedding VECTOR(1536), -- optional: for semantic search
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, path)
);
-- Indexes
CREATE INDEX idx_vfs_path_prefix ON vfs_files (tenant_id, path text_pattern_ops);
CREATE INDEX idx_vfs_fts ON vfs_files USING GIN (content_tsv);
CREATE INDEX idx_vfs_trgm ON vfs_files USING GIN (content gin_trgm_ops);
CREATE INDEX idx_vfs_embedding ON vfs_files USING hnsw (embedding vector_cosine_ops);
CREATE INDEX idx_vfs_tags ON vfs_files USING GIN (tags);Symlinks live in a separate vfs_symlinks table.
4.2 Multi-tenancy (RLS)
Every operation is scoped to a tenant_id. Row-Level Security enforces the boundary at the database layer, so two tenants can't see each other's files even at the SQL level.
# Alice's workspace
TENANT_ID=alice bun run ui
# Bob's workspace (can't see Alice's files)
TENANT_ID=bob bun run ui
# Error: no such file or directoryNo Unix users, no chroots, no per-session containers. One SQL predicate per query.
4.3 Migrations
The schema lives in src/db/schema.sql. Apply it with:
bun run migrateThe migration runner is idempotent, so it's safe to re-run after pulling schema changes.
5. Agent Integration
postgres-vfs plugs into Agent SDK agents via bash-tool. You get three tools (bash, readFile, writeFile) that behave like a normal sandboxed shell from the model's perspective.
5.1 bash-tool adapter
createPostgresVfsTool() returns a ready-to-use bash-tool toolkit scoped to a tenant. Drop it into generateText, streamText, or ToolLoopAgent.
5.2 createPostgresVfsTool API
import { createClient } from "postgres-vfs";
import { createPostgresVfsTool } from "postgres-vfs";
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
const sql = createClient("postgres://...");
const { tools } = await createPostgresVfsTool({
sql,
tenantId: "my-agent",
});
// tools contains: bash, readFile, writeFile
const { text } = await generateText({
model: anthropic("claude-sonnet-4-20250514"),
tools,
maxSteps: 10,
prompt: "Find all TODO comments in /projects and summarize them",
});5.3 Examples
Works with ToolLoopAgent just as well:
import { ToolLoopAgent } from "ai";
const agent = new ToolLoopAgent({
model: anthropic("claude-sonnet-4-20250514"),
tools,
});
const result = await agent.generate({
prompt: "Explore /docs and find information about authentication",
});Access the sandbox directly to pre-load files before the agent runs:
const { tools, sandbox } = await createPostgresVfsTool({ sql, tenantId: "my-agent" });
// Pre-load files before the agent starts
await sandbox.writeFiles([
{ path: "/docs/guide.md", content: guideMarkdown },
{ path: "/docs/api.md", content: apiReference },
]);
// Now the agent can explore them
const { text } = await generateText({ model, tools, prompt: "Summarize the API docs" });Runnable demos
Four end-to-end scripts live under examples/.
bash-tool-demo-generate.ts uses generateText (non-streaming). Waits for the whole run, then prints every tool call and the final artifact.
bash-tool-demo-stream.ts uses streamText (streaming). Prints text deltas, tool calls, and tool results as they arrive. Ships with four non-coding use cases as swappable seeds under examples/seeds/:
bun run examples/bash-tool-demo-stream.ts meetings # meeting notes → action items (default)
bun run examples/bash-tool-demo-stream.ts tickets # support ticket triage
bun run examples/bash-tool-demo-stream.ts research # literature review
bun run examples/bash-tool-demo-stream.ts contracts # contract risk reviewEach seed bundles the pre-loaded files, system + user prompts, tenant id, and output path in one module. Adding a new use case is a single file under examples/seeds/ plus one line in the stream demo's seed map.
bash-tool-demo-query.ts runs an ad-hoc streaming query against an existing tenant. Does not seed any files, so you can iterate on prompts against an already-populated workspace (a previous seed run, your own application data, etc.) without re-seeding each time. Tenant id, user prompt, and system prompt are CLI flags; long prompts can be loaded from a file with @path syntax:
# Query an existing seeded tenant
bun run examples/bash-tool-demo-query.ts \
--tenant bash-tool-demo-meetings \
--prompt "Who has the most action items and why?"
# Custom system prompt; load both prompts from files
bun run examples/bash-tool-demo-query.ts \
--tenant my-workspace \
--system @prompts/security-auditor.txt \
--prompt @prompts/task.txtRun with --help for the full flag list (--model, --max-steps, short aliases -t/-p/-s).
clear-seed.ts wipes the tenant rows for one seed (or all) when you've commented out the stream demo's built-in cleanup and want to reset between iterations:
bun run examples/clear-seed.ts meetings
bun run examples/clear-seed.ts all5.4 Custom shell commands
Beyond the 75+ standard bash commands, postgres-vfs adds a few that are database-native.
search runs a hybrid search combining FTS, trigram, and vector similarity into one ranked query.
search authentication # finds files discussing auth
search "database connection" --limit 5tag / untag / tags let you attach tags to files and query by tag.
tag /src/server.ts backend # add a tag
tag /src/database.ts backend
tag /src/database.ts database
tags backend # find files by tag
# /src/database.ts
# /src/server.ts
untag /src/server.ts backend # remove a tagrecent lists recently modified files, newest first.
recent 10 # last 10 modified filessummarize sets a human-readable summary that boosts search relevance.
summarize /src/server.ts "Express HTTP server with CRUD endpoints"
search server endpoints # summary boosts this file in results6. Web UI
See the Web UI screenshot above.
A three-column file manager for browsing, editing, searching, and tagging the VFS. Useful for inspecting what an agent has done without writing SQL.
6.1 Running the UI
bun run ui
# Open http://localhost:43216.2 Features
- Three-column layout: file tree, code viewer with line numbers, detail panel with metadata/tags/summary.
- Tenant selector to switch between isolated agent workspaces.
- File operations including create, edit, rename, delete files and folders.
- Upload and drag-drop. Upload local files or drag to move them between directories.
- Hybrid search with full-text + fuzzy results from the search bar.
- Markdown preview with a Raw/Preview toggle for
.mdfiles (headings, lists, code blocks, and task lists). - Tags and summaries editable from the detail panel.
- State persistence. Tenant, expanded folders, and selected file survive refresh via localStorage.
- Responsive. Mobile-friendly with slide-in sidebar and detail panel overlays.
- Breadcrumbs for clickable path segments and parent navigation.
6.3 REST API
The UI server exposes a REST API for programmatic file operations. See REST API Reference for the full endpoint list.
7. Reference
7.1 Environment variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgres://localhost:5432/postgres_vfs |
PostgreSQL connection string |
TENANT_ID |
default |
Default tenant for the web UI |
PORT |
4321 |
Web UI server port |
OPENAI_API_KEY |
(none) | Optional: enables the embedding pipeline (semantic search) |
EMBEDDING_MODEL |
text-embedding-3-small |
Optional: OpenAI embedding model |
7.2 Scripts
bun run ui # Web UI at http://localhost:4321
bun run migrate # Run database migrations
bun test # Run all tests
bun run build # Build for distribution7.3 Supported file types
The VFS stores content as Postgres TEXT. Any valid UTF-8 text file works. Binary files (null bytes) are not supported.
| Category | Extensions | Supported |
|---|---|---|
| Code | .ts, .js, .jsx, .tsx, .py, .go, .rs, .java, .c, .cpp, .h, .rb, .php, .swift, .kt, .sh |
Yes |
| Markup | .html, .xml, .svg, .vue, .svelte |
Yes |
| Styles | .css, .scss, .sass, .less |
Yes |
| Data | .json, .yaml, .yml, .toml, .ini, .env, .csv, .tsv |
Yes |
| Docs | .md, .mdx, .txt, .rst, .tex |
Yes |
| Config | .gitignore, .dockerignore, .editorconfig, Makefile, Dockerfile, .tf |
Yes |
| SQL | .sql |
Yes |
| Binary | .pdf, .png, .jpg, .gif, .zip, .exe, .wasm, .mp3, .mp4 |
No |
7.4 Project structure
A quick map of what lives where. Library code, tests, and the Web UI each have their own directory under src/.
postgres-vfs/
├── src/
│ ├── index.ts # Library exports
│ ├── utils.ts # Shared utilities
│ ├── db/
│ │ ├── schema.sql # PostgreSQL schema + indexes + extensions
│ │ ├── migrate.ts # Migration runner
│ │ └── client.ts # postgres.js client wrapper
│ ├── fs/
│ │ ├── postgres-fs.ts # IFileSystem implementation (20 methods)
│ │ ├── search.ts # FTS + trigram + optional vector search
│ │ ├── metadata.ts # Tags, summaries, embeddings
│ │ ├── embeddings.ts # Auto-embedding via OpenAI (optional)
│ │ └── path-utils.ts # Path normalization utilities
│ ├── commands/
│ │ └── index.ts # Custom commands (search, tag, recent, etc.)
│ ├── bash-tool/
│ │ ├── adapter.ts # Sandbox adapter (PostgresFs → bash-tool)
│ │ └── index.ts # createPostgresVfsTool() factory
│ └── ui/
│ ├── server.ts # Web UI HTTP server
│ ├── api.ts # API route handlers
│ └── index.html # Single-page frontend (vanilla JS)
├── test/
│ ├── fs.test.ts # PostgresFs unit tests
│ ├── bash-tool.test.ts # bash-tool adapter tests
│ ├── path-utils.test.ts # Path utility tests
│ └── commands.test.ts # Custom command tests
├── package.json
├── tsconfig.json
└── .env.example7.5 Dependencies
| Package | Purpose |
|---|---|
| just-bash | TypeScript bash interpreter with pluggable filesystem |
| bash-tool | AI SDK bash/readFile/writeFile tools |
| postgres | PostgreSQL client for Bun/Node |
| pgvector | Vector type support for optional semantic search |
8. Testing
Tests run against a live Postgres. Start a throwaway instance, migrate, then bun test.
# Start PostgreSQL (if not running)
docker run -d --name pgvfs-test \
-e POSTGRES_DB=postgres_vfs -e POSTGRES_PASSWORD=postgres \
-p 5432:5432 pgvector/pgvector:pg17
# Run migrations and tests
bun run migrate
bun test All pass
0 fail
Ran tests across 4 files.9. Scope and Limitations
just-bash is a pure TypeScript interpreter over an in-memory IFileSystem. It only implements commands that operate on the virtual filesystem. It has no network stack and cannot reach the host OS to spawn curl or any other binary.
The 75+ supported commands are filesystem and text utilities (grep, sed, awk, jq, find, sort, wc, pipes, redirects, etc.). Anything that requires sockets, subprocesses, or hardware (curl, wget, ssh, git, docker, python) is out of scope by design.
Need HTTP? Expose
fetchas a separate AI SDK tool alongsidecreatePostgresVfsTool'sbash/readFile/writeFile. The model usesbashfor workspace work and the dedicatedfetchtool for network calls, keeping the VFS sandboxed from the outside world.
10. Glossary
- bash-tool: Toolkit exposing
bash,readFile, andwriteFileas AI SDK tools. - just-bash: TypeScript bash interpreter with a pluggable
IFileSystembackend; parses commands, pipes, redirects, and globs. - IFileSystem: the ~20-method TypeScript interface just-bash calls;
PostgresFsimplements it against Postgres. - PostgresFs: this repo's
IFileSystemimplementation. Every filesystem op becomes a SQL query. - Sandbox adapter: the thin layer that bridges bash-tool's tool calls to
PostgresFs. - Tenant: an isolation boundary (one agent or user). Scoped via
tenant_idcolumn and row-level security. - RLS (Row-Level Security): Postgres feature that enforces per-tenant row visibility at the database layer.
- FTS (Full-Text Search): Postgres
tsvector+websearch_to_tsqueryindexed lexical search. - pg_trgm: Postgres trigram extension. Powers fuzzy and regex matching via a GIN index.
- pgvector: Postgres extension adding a
vectortype and similarity operators. Optional in this project. - HNSW: Hierarchical Navigable Small World, the approximate-nearest-neighbor index pgvector uses for fast vector search.
- Hybrid search: weighted combination of FTS + trigram + optional vector in a single ranked query.
- Embedding: a dense float vector representing file content semantically. Generated on write when
OPENAI_API_KEYis set. - Two-stage grep: coarse filter in Postgres (trigram index) followed by precise regex in JS. Makes
grep -rfast at scale. - Workspace: a tenant's private filesystem. Spins up in milliseconds because it's just a
tenant_id, not a container.
Acknowledgments
- ChromaFs for the virtual filesystem concept
- just-bash for the sandboxed bash interpreter
- agent-vfs for prior art taste on database-backed agent filesystems
Contributing
Contributions are welcome. Please open an issue first to discuss what you'd like to change, then submit a pull request.
