alibaba/open-code-review · error

rule file path %q escapes repo dir %q

Error message

rule file path %q escapes repo dir %q

What it means

readRuleFileSafe resolves a user-supplied rule file path via EvalSymlinks and enforces a confinement policy: if confineRoot is set and the resolved path is not inside it, the read is refused. This blocks a malicious or misconfigured rule reference from exfiltrating files outside the repository (rule content is sent to the LLM).

Source

Thrown at internal/config/rules/system_rules.go:643

	if os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "[ocr] WARNING: rule file not found: %s\n", rule)
	} else {
		fmt.Fprintf(os.Stderr, "[ocr] WARNING: cannot read rule file %s: %v\n", resolved, err)
	}
	return nil
}

// readRuleFileSafe reads and validates a rule file: extension whitelist, 512 KB cap,
// and symlink resolution. When confineRoot is non-empty, the resolved path must stay
// inside it. Returns the trimmed content on success.
func readRuleFileSafe(path string, confineRoot string) (string, error) {
	resolved, err := filepath.EvalSymlinks(path)
	if err != nil {
		return "", err
	}

	if confineRoot != "" && !pathutil.WithinBase(confineRoot, resolved) {
		return "", fmt.Errorf("rule file path %q escapes repo dir %q", resolved, confineRoot)
	}

	if !allowedRuleExts[strings.ToLower(filepath.Ext(resolved))] {
		return "", fmt.Errorf("unsupported extension %q, only .md/.txt/.markdown allowed", filepath.Ext(resolved))
	}

	const maxSize = 512 * 1024
	info, err := os.Stat(resolved)
	if err != nil {
		return "", err
	}
	if info.Size() > maxSize {
		return "", fmt.Errorf("file too large (%d bytes, max %d)", info.Size(), maxSize)
	}

	content, err := os.ReadFile(resolved)
	if err != nil {
		return "", err

View on GitHub (pinned to 5cf97d0d15)

Solutions

  1. Place the rule file inside the repository and pass a repo-root-relative path
  2. Replace absolute-path symlinks with real files (or symlinks whose resolved target is inside the repo)
  3. Copy the shared rule file into this repo instead of referencing it across repos
  4. If intentionally external, use --repo to set confineRoot appropriately or keep the rule in the repo root

Example fix

// before
ocr review --rule /home/me/shared-rules.md
// after
cp /home/me/shared-rules.md ./docs/review-rules.md
ocr review --rule docs/review-rules.md
Defensive patterns

Strategy: validation

Validate before calling

import "path/filepath"
func ruleInsideRepo(repoDir, rulePath string) (bool, error) {
    root, err := filepath.EvalSymlinks(repoDir)
    if err != nil { return false, err }
    resolved, err := filepath.EvalSymlinks(rulePath)
    if err != nil { return false, err }
    rel, err := filepath.Rel(root, resolved)
    if err != nil { return false, err }
    return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)), nil
}

Try / catch

_, err := tryReadRuleFile(rulePath, confineRoot)
if err != nil && strings.Contains(err.Error(), "escapes repo dir") {
    fmt.Fprintf(os.Stderr, "move %s inside the repo, or drop the --rule flag\n", rulePath)
    os.Exit(2)
}

Prevention

When it happens

Trigger: tryReadRuleFile is called with a path whose symlink-resolved target lies outside the repo dir — e.g. --rule /etc/passwd, --rule ../secrets.md, or a rule file that is a symlink to outside the repo.

Common situations: Pointing --rule at a shared rules file in another repo or a home directory; a symlinked file that used to be in-repo but now targets an absolute path elsewhere; running with a repoDir that differs from where rule files live.

Related errors


AI-assisted analysis of alibaba/open-code-review@5cf97d0d15 (2026-09-02). Data as JSON: /api/errors/8d58165bb87ad8c3. Report an issue: GitHub.