{"record":{"id":"7b861685e3f92301","repo":"gastownhall/beads","slug":"workspacegate-cannot-gate-q-the-guarded-path-mu","errorCode":null,"errorMessage":"workspacegate: cannot gate %q: the guarded path must be a named directory","messagePattern":"workspacegate: cannot gate %q: the guarded path must be a named directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workspacegate/gate.go","lineNumber":185,"sourceCode":"\tabs = filepath.Clean(abs)\n\n\t// Gate identity must be stable across the guarded directory being\n\t// absent, created, replaced, or recreated — that is the point of\n\t// placing the gate beside it. So identity derives from the\n\t// canonicalized PARENT plus the literal base name, never from\n\t// resolving the guarded path itself: full-path resolution would\n\t// silently select a different gate once the directory appears as a\n\t// symlink, letting two exclusive holders coexist. The flip side is\n\t// that a guarded directory that IS a symlink has no stable identity\n\t// under this scheme, so it is refused outright rather than gated\n\t// ambiguously.\n\tif fi, err := os.Lstat(abs); err == nil && fi.Mode()&os.ModeSymlink != 0 {\n\t\treturn Gate{}, fmt.Errorf(\"workspacegate: %s is a symlink; gate the physical directory it points to\", abs)\n\t}\n\tparent, base := filepath.Split(abs)\n\tswitch base {\n\tcase \"\", \".\", \"..\":\n\t\treturn Gate{}, fmt.Errorf(\"workspacegate: cannot gate %q: the guarded path must be a named directory\", dir)\n\t}\n\tcanonParent, err := filepath.EvalSymlinks(filepath.Clean(parent))\n\tif err != nil {\n\t\treturn Gate{}, fmt.Errorf(\"workspacegate: gate parent %s must exist: %w\", parent, err)\n\t}\n\treturn Gate{path: filepath.Join(canonParent, gateFileName(base))}, nil\n}\n\n// ForWorkspace returns the gate guarding a workspace's .beads directory\n// (pass the .beads directory itself). The gate file is a sibling of\n// .beads; bd's project gitignore management covers \"*.gate.lock*\".\nfunc ForWorkspace(beadsDir string) (Gate, error) { return forDir(beadsDir) }\n\n// ForPhysicalRoot returns the gate guarding a physical database root (a\n// dolt server root such as .beads/dolt or ~/.beads/shared-server/dolt).\n// Distinct workspaces that point at the same physical root resolve to the\n// same gate file, which is the point: a workspace-level gate alone cannot\n// stop workspace B from restarting the server workspace A is draining.","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L167-L203","documentation":"forDir refuses to build a gate for a path whose final element is empty, \".\", or \"..\". Gate identity derives from the canonicalized parent plus the literal base name, so a path with no named base component has no stable gate file name and cannot be fenced. This is a programming/caller error, not a transient condition.","triggerScenarios":"Calling ForWorkspace or ForPhysicalRoot with a path ending in a separator (e.g. \"/repo/.beads/\"), with \".\" or \"..\" as the last element, or with the filesystem root \"/\" — after filepath.Abs+Clean such inputs reduce to an empty or dot/dotdot base.","commonSituations":"Concatenating dir + \"/\" before passing a .beads path; user-supplied config values like beadsDir=\"..\" or \".\"; computing a path from os.Getwd() at the volume root; shell-style paths copied with trailing slashes into code or config.","solutions":["Strip any trailing separator before calling: dir = filepath.Clean(dir) (Clean removes trailing slashes unless the path is \"/\").","Pass the concrete .beads directory path (e.g. /repo/.beads), not \".\", \"..\", the repo root's parent, or \"/\".","If the caller only knows the workspace root, join the .beads name: filepath.Join(root, \".beads\") before calling ForWorkspace.","For \"/\" or a drive root there is no valid guard — reject the input upstream instead of gating it."],"exampleFix":"// before\ng, err := workspacegate.ForWorkspace(cfg.BeadsDir + \"/\")\n// after\ng, err := workspacegate.ForWorkspace(filepath.Clean(cfg.BeadsDir))","handlingStrategy":"validation","validationCode":"func validateGatePath(dir string) error {\n    abs, err := filepath.Abs(dir)\n    if err != nil { return err }\n    abs = filepath.Clean(abs)\n    base := filepath.Base(abs)\n    if base == \".\" || base == \"..\" || abs == \"/\" {\n        return fmt.Errorf(\"cannot gate %q: need a named directory\", dir)\n    }\n    return nil\n}","typeGuard":"func isNamedDirPath(dir string) bool {\n    base := filepath.Base(filepath.Clean(dir))\n    return base != \".\" && base != \"..\" && base != \"/\" && base != string(filepath.Separator)\n}","tryCatchPattern":null,"preventionTips":["Always filepath.Clean() user- or config-supplied paths before gating.","Never append a trailing separator to a directory path in Go code.","Pass the .beads directory itself, never \".\", \"..\", or a volume root.","Reject root paths (\"/\", drive roots) in config parsing before they reach the gate."],"tags":["go","path-validation","workspacegate"],"backgroundTag":"invalid-path-argument","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}