Skip to content

Path protection false positives: substring matching matches 'logout/' for 'out/' #1

Description

@log0u7

Summary

Non-glob protected path entries are matched with a raw String.includes() against the full path. Any directory pattern whose name appears as a substring of another directory name triggers false positives. In particular, the built-in out/ rule matches every path under a user's /home/lOGOUT/ home directory, so legitimate file writes get blocked with DAMAGE_CONTROL_BLOCKED: Cannot write protected path "out/".

Environment

  • opencode-damage-control: 1.5.0 (same code on current main)
  • opencode: 1.18.25 (Linux, x86_64)
  • OS: Ubuntu (Linux)

Real-world impact

opencode's plan mode writes plan files under either <repo>/.opencode/plans/<ts>-<slug>.md or ~/.local/share/opencode/plans/<ts>-<slug>.md. On a machine whose username is logout (/home/logout/...), every single plan-file write was rejected:

DAMAGE_CONTROL_BLOCKED: Cannot write protected path "out/"
Protection level: readOnly

Observed in practice: 9 consecutive blocked writes within 25 minutes (with the agent retrying after each block), all targeting paths like:

  • /home/logout/.local/share/opencode/plans/1787824560041-hidden-comet.md
  • /home/logout/projets/logout/worms/.opencode/plans/1787836297164-mighty-comet.md
  • /home/logout/projets/gitlab/6admin.io/blog/.opencode/plans/1787824560041-hidden-comet.md

The only escape currently is disabling the rule entirely via paths.override: { "out/": "none" }, which removes a legitimate guardrail (writes to actual out/ build directories are no longer protected at all).

Root cause

src/patterns.ts, checkPathProtection() (non-glob branch):

// Non-glob: expand home and do prefix/substring matching
const expandedPath = p.path.replace('~', home);
if (filePath.includes(expandedPath) || filePath.includes(p.path)) {
    return p;
}

filePath.includes("out/") is a raw substring test: "…/home/lOGOUT/.local/…" contains out/, so the rule fires. The doc comment above the function says directory patterns should "check if filePath starts with or contains the dir", which reads as segment-boundary semantics, but the implementation is plain substring matching.

The same flaw exists in commandReferencesPath() (shell-command matching), so rm -rf ~/logout-proj/dist would also be flagged for the dist/ rule.

Affected built-in directory rules include at least: dist/, build/, out/, venv/, target/, .git/, .github/, plus any user-defined trailing-slash entry. Examples of colliding real-world directory names: logout/, about/, output-logs/ (contains out/? no, but pour/, thought/, without/ do), distance/, builders/, convenience/, objective/.

Minimal repro

import { checkPathProtection, DEFAULT_PROTECTED_PATHS } from "./dist/patterns.js";

// false positive: matches the built-in out/ rule
console.log(checkPathProtection("/home/logout/.local/share/opencode/plans/plan.md", DEFAULT_PROTECTED_PATHS));
// => { path: 'out/', level: 'readOnly' }

// expected: null

Proposed fix

For directory patterns (entries ending with /), match on path-segment boundaries instead of substrings:

if (p.path.endsWith('/')) {
    const dir = expandedPath.replace(/\/+$/, '');
    const normalized = filePath.replace(/\/+$/, '');
    if (normalized === dir || normalized.startsWith(dir + '/') || normalized.includes('/' + dir + '/')) {
        return p;
    }
    continue;
}
  • "/home/logout/x" no longer matches out/ (no /out/ segment)
  • "/project/out/a.js" and relative "out/a.js" still match
  • literal entries without a trailing slash (~/.ssh, ~/.config/opencode) and glob entries (.env*, *.pem) keep their current semantics

The identical segment-boundary logic should be applied to commandReferencesPath() for non-glob entries.

Happy to open a PR with the fix + tests if you are open to it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions