multica-ai/multica · error
task-local CLI config directory %q escapes root %q
Error message
task-local CLI config directory %q escapes root %q
What it means
A defensive invariant inside the task-local permission walk: the code ascends from the config directory via filepath.Dir until it reaches the task root, and if it reaches the filesystem root ('/') without matching the configured MULTICA_TASK_CONFIG_ROOT it aborts. This happens when the resolved config path is not actually under the task root — e.g. the root env var and the profile path disagree, or symlinks make the textual prefix comparison fail.
Source
Thrown at server/internal/cli/config.go:344
}
if err := os.MkdirAll(dir, dirMode); err != nil {
return fmt.Errorf("create CLI config directory: %w", err)
}
if dirMode == 0o700 {
root, _, err := multicaConfigRoot()
if err != nil {
return fmt.Errorf("resolve task-local CLI config root: %w", err)
}
for current := dir; ; current = filepath.Dir(current) {
if err := os.Chmod(current, 0o700); err != nil {
return fmt.Errorf("restrict task-local CLI config directory: %w", err)
}
if current == root {
break
}
parent := filepath.Dir(current)
if parent == current {
return fmt.Errorf("task-local CLI config directory %q escapes root %q", dir, root)
}
}
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("encode CLI config: %w", err)
}
// Write to a temp file in the same directory, then rename for atomicity.
tmp, err := os.CreateTemp(dir, ".config-*.json.tmp")
if err != nil {
return fmt.Errorf("create temp config file: %w", err)
}
tmpPath := tmp.Name()
if _, err := tmp.Write(append(data, '\n')); err != nil {
tmp.Close()
os.Remove(tmpPath)
return fmt.Errorf("write temp config file: %w", err)View on GitHub (pinned to 2c0912b6ec)
Solutions
- Set MULTICA_TASK_CONFIG_ROOT to the cleaned, canonical absolute path with no trailing slash or '..' components (e.g. /var/lib/multica/tasks)
- Resolve symlinks first: realpath the intended root and export that value
- Verify with a quick check that the config dir path starts with the exported root value plus a path separator
- If the escaping is unexpected, inspect CLIConfigPathForProfile's output for the active profile to see which side is wrong
Example fix
# before export MULTICA_TASK_CONFIG_ROOT=/var/lib/multica/tasks/ # config dir resolves to /var/lib/multica/tasks/profiles/dev -> root never matches # after export MULTICA_TASK_CONFIG_ROOT=$(realpath /var/lib/multica/tasks)
Defensive patterns
Strategy: validation
Validate before calling
root := filepath.Clean(os.Getenv("MULTICA_TASK_CONFIG_ROOT"))
if resolved, err := filepath.EvalSymlinks(root); err == nil {
os.Setenv("MULTICA_TASK_CONFIG_ROOT", resolved)
} Prevention
- Export the canonicalized (realpath) task root with no trailing slash
- Avoid symlinking the task root
- Add a startup assertion that the computed config path is under the exported root
When it happens
Trigger: MULTICA_TASK_CONFIG_ROOT containing a path that is not a lexical prefix of the computed config dir: trailing-slash vs cleaned-path mismatches, a symlinked home directory so the cleaned path never equals the root string, or '..' components surviving in the env var.
Common situations: Setting MULTICA_TASK_CONFIG_ROOT=/var/lib/multica/tasks/ (trailing slash) while the config path resolves to /var/lib/multica/tasks/profiles/x; /home pointing at /users via symlink on macOS.
Related errors
- resolve task-local CLI config root: %w
- local_directory: local_path is empty
- agent execution context requires MULTICA_TOKEN to be a task-
- server URL not set: use --server-url flag, MULTICA_SERVER_UR
- daemon-managed task requires a task-local Multica config roo
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/d50b0ba1b322b3c8.
Report an issue: GitHub.