Guidance for Claude Code when working with this repository.
TigerFS is a FUSE-based filesystem that exposes PostgreSQL databases as mountable directories. Users interact with tables, rows, and columns using Unix tools (ls, cat, grep, rm) instead of SQL.
Primary Goal: Enable Claude Code to explore and manipulate database data using Read/Glob/Grep operations.
Full specification: See docs/spec.md for complete technical details including filesystem structure, data formats, SQL patterns, and error codes.
# Build
go build -o bin/tigerfs ./cmd/tigerfs
# Test
go test ./... # All tests
go test -race ./... # With race detection
go test -run TestName ./path # Specific test
# Synth app tests
go test ./internal/tigerfs/fs/synth/... # Unit (synth package)
go test -run "^TestSynth_" ./internal/tigerfs/fs/... ./test/integration/... # Unit (synth_ops) + integration
# Before committing (required)
go fmt ./... && go vet ./... && go test ./... && go mod tidyThis is primarily a Go codebase. Follow Go idioms: error handling with explicit returns, table-driven tests, and gofmt formatting.
| Package | Purpose |
|---|---|
cmd/tigerfs/ |
Entry point with signal handling |
internal/tigerfs/cmd/ |
Cobra commands (pure functional builders) |
internal/tigerfs/fs/ |
Shared filesystem logic (path parsing, operations, stat cache) |
internal/tigerfs/fuse/ |
Linux FUSE adapter |
internal/tigerfs/nfs/ |
macOS NFS adapter |
internal/tigerfs/db/ |
PostgreSQL client and queries |
internal/tigerfs/backend/ |
Cloud backend resolution (Tiger, Ghost, postgres://) |
internal/tigerfs/mount/ |
Mount registry for tracking active mounts |
internal/tigerfs/config/ |
Viper-based configuration |
internal/tigerfs/logging/ |
Zap structured logging |
internal/tigerfs/format/ |
Data serialization (TSV, CSV, JSON) |
macOS uses an in-process NFS v3 server (go-nfs); Linux uses FUSE (go-fuse). Both delegate to fs.Operations for all filesystem logic -- adapters are thin translation layers. New filesystem behavior goes in fs/, not in the adapters. Note: fuse/ still contains legacy direct-to-DB code paths; the active default path routes through fs/.
go-nfs write model: NFS v3 is stateless -- go-nfs fabricates Open/Write/Close per WRITE RPC. Each Close commits the buffer to the DB. Multi-chunk writes cause repeated commits until the final Close. Stat must return accurate file sizes for NFS reads (GETATTR before READ).
Capability directories (.filter, .order, .first, .last, .export, .columns) accumulate query state as users navigate deeper. The path products/.filter/in_stock/true/.order/price/.first/10/.export/json builds a single SQL query. State is tracked in FSContext and parsed by fs/path.go.
Tables whose names start with _ (e.g., _blog, _docs) can be exposed as synthesized views -- markdown files, task lists, or plain text snippets. The synth layer (fs/synth/) maps filesystem operations to table rows using format-specific conventions. See docs/file-first.md.
Always use db.QuoteIdent() and db.QuoteTable() when interpolating identifiers into SQL strings. Never use fmt.Sprintf with "%s" to quote identifiers -- it does not escape embedded double quotes, which can cause SQL syntax errors or injection. Within the db package, use the short aliases qi() and qt().
| Helper | Purpose | Example |
|---|---|---|
db.QuoteIdent(name) / qi(name) |
Quote a single identifier (column, table, or schema) | QuoteIdent("email") -> "email" |
db.QuoteTable(schema, table) / qt(schema, table) |
Quote a schema-qualified table reference | QuoteTable("public", "users") -> "public"."users" |
When to use: Any column, table, or schema name interpolated into SQL via fmt.Sprintf. From outside the db package, use the exported names. Do not use for SQL keywords, parameterized values ($1), or pre-built clause strings.
Critical pattern: All commands use functional builders with zero global state. See any existing buildXxxCmd() function for the pattern.
Rules:
- No global variables for commands, flags, or state
- Every command built by a
buildXxxCmd()function - Flag variables declared locally within builder functions
- Bind flags to viper in
PersistentPreRunE/PreRunE, not at build time
Precedence (low to high): Defaults → Config file (~/.config/tigerfs/config.yaml) → Environment (TIGERFS_*) → Flags
TLS enforcement: TigerFS enforces sslmode=require for non-localhost connections. The --insecure-no-ssl flag or InsecureNoSSL config disables this. Localhost (127.0.0.1, ::1, Unix sockets) defaults to sslmode=disable when no sslmode is specified; users can override with an explicit ?sslmode=require.
Rules:
- Always use the
Configstruct - never read from viper directly - Load config once at command start, pass down to functions
- Bind flags in
PreRunE, not at build time
Full configuration options: See docs/spec.md § Configuration System
Design principle: A write on one mount must be immediately visible to reads on any other mount of the same database. PostgreSQL is the single source of truth.
Multiple TigerFS mounts may connect to the same database concurrently. This means:
- Never cache content: Row values, export data, column values -- anything returned by
ReadFile/cat/read()must always query the DB fresh. - Cache metadata only: Sizes, permissions, directory entries, column names/types, primary keys, table lists. These change rarely and have short TTLs.
- Stat cache keys must be unique per path: Different pipeline paths (e.g.,
.export/jsonvs.filter/active/true/.export/json) must not share cache entries, even on the same table. - Pattern:
Statmay use caches.ReadFilemust always hit the DB. - Audit invalidation on every new write path: any write must call
statCache.invalidate(schema, table)andpathCache.invalidate(schema, table)with the user schema (notsynth.TigerFSSchema); a schema-key mismatch silently leaves stale entries cached for up to 2 seconds and surfaces as cross-mount read inconsistencies.
Uses zap with two modes:
- Production (default): Warn+ level, minimal output
- Debug (
--debug): All levels, colored, with timestamps and caller info
logging.Debug("message", zap.String("key", "value"))
logging.Info("message", zap.Int("count", 42))
logging.Warn("message", zap.Error(err))
logging.Error("message", zap.Error(err))FUSE can only return errno codes, not messages. Log detailed errors before returning errno - output goes to stderr and user sees it:
logging.Error("invalid task number",
zap.String("number", number),
zap.String("hint", "must be positive integers (e.g., 1, 1.2)"))
return syscall.EINVAL$ mv 1-foo-o.md a-foo-o.md
{"message":"invalid task number","number":"a","hint":"must be positive integers (e.g., 1, 1.2)"}
mv: cannot move '1-foo-o.md' to 'a-foo-o.md': Invalid argument
logging.Error()for failures returning errnologging.Warn()for successful operations user should know about- Include
zap.String("hint", "...")for guidance
See internal/tigerfs/fuse/control_files.go for examples.
For each implementation task:
- Write unit tests for all new functions (required)
- Propose integration tests for workflows crossing package boundaries (ask before implementing)
Integration tests use testcontainers-go for PostgreSQL. See test/integration/ for examples.
Stress runs (bin/tigerfs-stress start) may emit [warn iter ...] readLatestLogID regressed after ... lines. These are expected — they indicate the macOS NFS layer (go-nfs library or kernel client, ruled out at every TigerFS layer) returned a stale .log/.last/N/.export/json snapshot after a heavy commit. The runner's monotonic helper retries up to 2.5s and falls back to the prior known-good lastLogID; the run continues correctly. The end-of-run "Monotonicity Warnings" section summarizes rate, recovery distribution, and op kinds preceding regressions. Do not treat these as bugs to fix in TigerFS; see the iter-107 investigation arc on feat/undo (commits 85a2495 → f59cfcf) for context.
Integration tests that mount the filesystem work with both FUSE (Linux) and NFS (macOS) — the mount method is auto-detected. Name tests by what they test, not the mount method:
| Prefix | Meaning | When to use |
|---|---|---|
TestMount_, TestWrite_, TestDDL_, etc. |
Generic — works with any mount method | Default for all mount-based tests |
TestNFS_ |
NFS-only — skipped on FUSE/Linux | Only if the test exercises NFS-specific behavior that cannot work with FUSE |
TestFUSE_ |
FUSE-only — skipped on NFS/macOS | Only if the test exercises FUSE-specific behavior that cannot work with NFS |
Do not prefix tests with a mount method name unless they are truly specific to that method.
All synth-related unit tests (in internal/tigerfs/fs/synth/ and internal/tigerfs/fs/) use the TestSynth_ prefix. This enables running all synth tests across packages with a single filter:
go test -run "^TestSynth_" ./internal/tigerfs/fs/... ./test/integration/...- Core packages (
db,fs,fuse,nfs): Full function docs, field comments, "why" explanations, edge cases - Commands, config, utilities: Function docs with params/returns
- Tests: Brief function comments only
- Comment-code consistency: Always check for mismatches between comments and code. Stale comments mislead -- flag to user before changing. When modifying files, update comments for code you touch but don't document the entire file.
Before implementing any feature, confirm the spec/requirements with me first. Ask clarifying questions about edge cases and expected behavior before writing code.
- Create
internal/tigerfs/cmd/<command>.gowithbuildXxxCmd()function - Add to root:
cmd.AddCommand(buildXxxCmd())inroot.go - Write unit tests for pure functions
- Test:
go run ./cmd/tigerfs <command> --help
- Add field to
Configstruct inconfig/config.go - Set default in
Init() - Document in
docs/spec.md§ Configuration System
- Implement method on
FSstruct ininternal/tigerfs/fuse/ - Add SQL query generation in
internal/tigerfs/db/query.go - Add format conversion in
internal/tigerfs/format/ - Write unit tests for each layer
- Propose integration tests with recommendation (ask user before implementing)
Keep git operations simple:
- Add files explicitly by name - never use
git add -Aorgit add . - Make simple commits - just
git add <files>andgit commit - No complex git operations - avoid
git rebase,git commit --amend,git reset,git push --force, or any destructive commands - If commits need fixing, let the user handle it manually
After exiting plan mode, rename the plan file from its auto-generated name to a short descriptive name (e.g., stat-cache-pipeline-isolation.md). Do this before starting implementation. Periodically delete completed plan files to avoid clutter.
# Always run before committing
go fmt ./... && go vet ./... && go test ./... && go mod tidyCommit message format:
feat: add CSV format support
fix: handle NULL values in JSONB columns
docs: update configuration examples
test: add integration tests for index navigation
See docs/release-process.md for the full release checklist (tests, CHANGELOG, goreleaser, tagging, GitHub release notes).
| Topic | Location |
|---|---|
| Filesystem structure & paths | docs/spec.md § Filesystem Structure |
| Data formats (TSV, CSV, JSON) | docs/spec.md § Data Representation |
| SQL query patterns | docs/spec.md § Appendix D |
| Error codes (errno mapping) | docs/spec.md § Error Handling |
| CLI commands & flags | docs/spec.md § CLI Interface |
| Tiger Cloud integration | docs/spec.md § Tiger Cloud Integration |
| Implementation tasks | docs/implementation/implementation-tasks.md |
| Task checklist | docs/implementation/implementation-tasks-checklist.md |
Keep docs/implementation/implementation-tasks-checklist.md in sync with docs/implementation/implementation-tasks.md:
- Task completed: Mark ⬜ → ✅ and update Summary table
- Tasks modified: New tasks added, tasks removed, or tasks renumbered