Skip to content

Gotchas and operating rules

These pitfalls are drawn from the canonical docs/general/gotchas.md. Every entry below has a verified source — no invented issues.

Old instructions may reference go-libsql, github.com/mattn/go-sqlite3, -tags=libsqlite3, sqlite-vector, or foxctl-cgo. Do not revive that lane. Turso is the canonical SQLite-family storage path and builds through the normal non-CGO targets:

Terminal window
make build
make test

tursogo and dspy-go may still appear as transitive modules, but foxctl does not import them as storage drivers. With CGO_ENABLED=0, the full suite passes. Do not treat the indirect module entry as a reason to restore CGO builds.

The skill loader looks for a binary named bin (or bin-cgo), not custom names.

Terminal window
# Correct — loader will find it
go build -o ~/.foxctl/skills/my/skill/bin ./skills/my_skill
# Wrong — loader won't find it
go build -o ~/.foxctl/skills/my/skill/my_skill ./skills/my_skill

Building creates the binary in the source directory, not the install location. You must build and install:

Terminal window
# Install all skills
make skills-install
# Or install a single skill
CGO_ENABLED=0 go build -o ~/.foxctl/skills/code/symbols/bin ./skills/code_symbols

gather_context may return odd results, miss route mounts, or show repo_index with zero hits when the repo graph index is missing, empty, stale, or built for a different commit.

Check index status before judging search quality:

Terminal window
foxctl index repo status --workspace /path/to/repo

If nodes_total is 0 or index_matches_head is false, rebuild for the repo’s languages:

Terminal window
foxctl index repo build --workspace /path/to/repo --go=false --typescript

For dirty worktrees, the index records HEAD plus dirty state. gather_context should still use live-overlay providers for changed files, but indexed route/import/symbol closure only works when the repoindex has useful nodes.

Skills must explicitly load .env files. API keys will not be found without this call:

import "github.com/joshka0/foxctl/internal/platform/config"
func main() {
config.LoadDotEnv() // BEFORE os.Getenv()
baseURL := os.Getenv("FOXCTL_EMBEDDING_BASE_URL")
}

If ~/.foxctl/.env is a symlink to the repo’s .env file, it breaks in sandboxed or remote environments where the repo path doesn’t exist.

Terminal window
# Copy instead of symlinking
make env-sync # Copies repo .env → ~/.foxctl/.env
# Verify it's a real file
ls -la ~/.foxctl/.env
# Should show: -rw------- (not lrwxr-xr-x)

The .env loader checks these locations in order:

  1. ~/.foxctl/.env (global defaults)
  2. $FOXCTL_HOME/.env (if set)
  3. $PWD/.env (project overrides)

Memory queries return empty results when run from the wrong workspace. Queries are scoped by workspace path:

Terminal window
# Run from project directory
cd /path/to/project
foxctl memory search "auth"
# Or specify workspace explicitly
foxctl run memory/query --input '{"workspace": "/path/to/project"}'

Mixing embedding providers with different vector dimensions causes search failures:

ProviderDimensions
Qwen3 Embedding 8B4096
Qwen3 Embedding 4B2560
Qwen3 Embedding 0.6B1024
Gemini3072

Use the same provider for storage and queries. When switching providers, rebuild the affected vector store.

Tilde (~) is not expanded in environment variables:

Terminal window
# Wrong
FOXCTL_OBS_DIR=~/.foxctl/observability
# Correct
FOXCTL_OBS_DIR=$HOME/.foxctl/observability

Memories not persisting usually means using the cache path instead of the storage path:

// Correct — persistent storage
store, err := memory.Open(ctx, cfg.Storage.Root, cfg.Paths.CAS)
// Wrong — cache is ephemeral
store, err := memory.Open(ctx, cfg.Paths.Cache, cfg.Paths.CAS)

Compile errors on casStore.Put() are caused by wrong argument count or types:

// Correct — 4 arguments, returns CASObject
obj, err := casStore.Put(ctx, bytes.NewReader(data), "application/json", []string{"tag"})
digest := obj.Digest // "sha256:..."
// Wrong — missing arguments
digest, err := casStore.Put(ctx, data)

Session JSONL files are compressed as .jsonl.gz. Attempting to read them as plain text will fail:

if strings.HasSuffix(path, ".gz") {
gzReader, err := gzip.NewReader(file)
if err != nil {
return err
}
defer gzReader.Close()
reader = gzReader
}

Session summaries are per-window, not per-session. If summaries appear empty or missing, re-run summarization for all windows:

Terminal window
foxctl run session/summarize --input '{"session_id": "..."}'

Room messages stored but not visible in tmux

Section titled “Room messages stored but not visible in tmux”

A message can be durably written while one participant pane does not show it. Two common failure modes:

  1. The live room loop is running in the wrong workspace, polling a different board.db.
  2. The room member’s actor_id does not match the actual tmux pane label.

Check all three layers:

Terminal window
# 1. Verify the live loop workspace
ps -Ao pid=,command= | rg 'foxctl room loop'
lsof -a -p <loop-pid> -d cwd
# 2. Verify room membership includes pane_id
foxctl room show <room-id> --workspace /path/to/workspace
# 3. Read the pane directly
foxctl mux read <pane-id> --lines 120

For tmux-backed room relay, pane_id is the authoritative delivery target. actor_id is for room semantics and matching.

A potential race condition exists between path validation and file reading. Open the file immediately after validation:

// Correct — open immediately, then validate
path, err := pathValidator.ValidatePath(requested)
if err != nil { return err }
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
// Re-validate for symlink escapes
resolved, err := filepath.EvalSymlinks(path)
if _, err := pathValidator.ValidatePath(resolved); err != nil {
return fmt.Errorf("symlink escape: %w", err)
}
data, err := io.ReadAll(f)
// Wrong — race window between validate and read
path, _ := pathValidator.ValidatePath(requested)
data, _ := os.ReadFile(path)

Using os.Getwd() directly returns the wrong workspace in sandboxed execution. Use platform workspace detection instead:

import "github.com/joshka0/foxctl/internal/platform/workspace"
ws := workspace.Detect("") // Handles FOXCTL_WORKSPACE, git root, etc.

These rules prevent avoidable breakage in daily foxctl work:

RuleWhy it matters
Prefer ./bin/foxctl in this checkout when PATH is ambiguousAvoids bundled-wrapper command gaps
Preserve JSON envelope shapeHooks, GUIs, and golden tests depend on it
Keep WASI skill network policy at network: "none"Maintains Core v1 isolation
Use CAS for large outputs (>64 KB)Prevents bloated envelopes and memory pressure
Use structured shell for noisy read-only retrievalKeeps agent context compact
Do not route behavior with keyword heuristicsAvoids brittle hidden policy
Run make check-doc-links after markdown changesCI enforces docs link hygiene
Never change meta.* envelope fields without spec updatesDownstream tooling relies on stable shape