docs/REPO_CONTEXT.md
This document explains how beads resolves repository context when commands run from
different directories than where .beads/ lives.
Git commands must run in the correct repository, but users may invoke bd from:
BEADS_DIR environment variable).beads/)Without centralized handling, each command must implement its own path resolution, leading to bugs when assumptions don't match reality.
The RepoContext API provides a single source of truth for repository resolution:
import "github.com/steveyegge/beads/internal/beads"
rc, err := beads.GetRepoContext()
if err != nil {
return err
}
// Run git in beads repository (not CWD)
cmd := rc.GitCmd(ctx, "status")
output, err := cmd.Output()
| Method | Use Case | Example |
|---|---|---|
GitCmd() | Git commands for beads operations | git add .beads/, git push |
GitCmdCWD() | Git commands for user's working repo | git status (show user's changes) |
RelPath() | Convert absolute path to repo-relative | Display paths in output |
The distinction matters when BEADS_DIR points to a different repository:
rc, _ := beads.GetRepoContext()
// GitCmd: runs in the beads repository
// Use for: committing .beads/, pushing/pulling beads data
cmd := rc.GitCmd(ctx, "add", ".beads/issues.jsonl")
// GitCmdCWD: runs in user's current repository
// Use for: checking user's uncommitted changes, status display
cmd := rc.GitCmdCWD(ctx, "status", "--porcelain")
CWD is inside the repository containing .beads/:
/project/
├── .beads/
├── src/
└── README.md
$ cd /project/src
$ bd dolt push
# GitCmd() runs in /project (correct)
User is in one repository but managing beads in another:
$ cd /repo-a # Has uncommitted changes
$ export BEADS_DIR=/repo-b/.beads
$ bd dolt push
# GitCmd() runs in /repo-b (correct, not /repo-a)
This pattern is common for:
User is in a worktree but .beads/ lives in main repository:
/project/ # Main repo
├── .beads/
├── .worktrees/
│ └── feature-branch/ # Worktree (CWD)
└── src/
$ cd /project/.worktrees/feature-branch
$ bd dolt push
# GitCmd() runs in /project (main repo, where .beads lives)
Both worktree and BEADS_DIR can be active simultaneously:
$ cd /repo-a/.worktrees/branch-x
$ export BEADS_DIR=/repo-b/.beads
$ bd dolt push
# GitCmd() runs in /repo-b (BEADS_DIR takes precedence)
| Field | Description |
|---|---|
BeadsDir | Actual .beads/ directory (after following redirects) |
RepoRoot | Repository root containing BeadsDir |
CWDRepoRoot | Repository root containing user's CWD (may differ) |
IsRedirected | True if BEADS_DIR points to different repo than CWD |
IsWorktree | True if CWD is in a git worktree |
GitCmd() disables git hooks and templates to prevent code execution in
potentially malicious repositories:
cmd.Env = append(os.Environ(),
"GIT_HOOKS_PATH=", // Disable hooks
"GIT_TEMPLATE_DIR=", // Disable templates
)
This protects against scenarios where BEADS_DIR points to an untrusted
repository that contains malicious .git/hooks/ scripts.
GetRepoContext() validates that BEADS_DIR does not point to sensitive
system directories:
/etc, /usr, /var, /root (Unix system directories)/System, /Library (macOS system directories)Temporary directories (e.g., /var/folders on macOS) are explicitly allowed
for test environments.
For CLI commands, GetRepoContext() caches the result via sync.Once because:
For the Dolt server (long-running process), this caching is inappropriate:
The server uses GetRepoContextForWorkspace() for fresh resolution:
// For server mode: fresh resolution per-operation (no caching)
rc, err := beads.GetRepoContextForWorkspace(workspacePath)
// Validation hook for detecting stale contexts
if err := rc.Validate(); err != nil {
// Context is stale, need fresh resolution
}
This function:
func doGitOperation(ctx context.Context) error {
// Each function resolved paths differently
beadsDir := beads.FindBeadsDir()
redirectInfo := beads.GetRedirectInfo()
var repoRoot string
if redirectInfo.IsRedirected {
repoRoot = filepath.Dir(beadsDir)
} else {
repoRoot = getRepoRootForWorktree(ctx)
}
cmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "status")
// ...
}
func doGitOperation(ctx context.Context) error {
rc, err := beads.GetRepoContext()
if err != nil {
return err
}
cmd := rc.GitCmd(ctx, "status")
// ...
}
rc.GitCmd() or rc.GitCmdCWD()beads.ResetCaches() in test cleanupTests use beads.ResetCaches() to clear cached context between test cases:
func TestSomething(t *testing.T) {
t.Cleanup(func() {
beads.ResetCaches()
git.ResetCaches()
})
// Test code...
}
sync.Once for CLI efficiencycmd.Dir pattern (not -C flag) for Go-idiomatic execution