Skip to content

security

Ary Rabelo edited this page Jul 22, 2026 · 3 revisions

Now I have enough grounding to write the page.

Security

Relevant source files

  • SECURITY.md
  • src/repodocs/_util.py
  • src/repodocs/backend.py
  • src/repodocs/publish.py

RepoDocs treats the target repository as untrusted input at every boundary: repository content is read, fed to LLM backends, and its generated output is scanned before it ever reaches a network endpoint. This page describes the trust boundaries the code enforces, drawn from SECURITY.md and the implementation in _util.py, backend.py, and publish.py.

Sources: SECURITY.md:L13-L25

Repository scanning boundary

RepoDocs only reads files it classifies as source or manifests. is_source() accepts known source extensions or extension-less files whose first two bytes are #! (a shebang), and is_test() classifies files under tests/test directories or matching test-name conventions so planning can separate application code from test code. SKIP_DIRS (.git, node_modules, dist, build, __pycache__, vendor, .venv, venv) and MAX_DEPTH = 5 / MAX_COMPONENTS = 6 bound how deep and how wide the repository walk goes, limiting exposure to generated or vendored trees and pathological directory structures.

Sources: src/repodocs/_util.py:L11-L66

Path resolution back into the repository is centralized in safe_repo_file(), which resolves a repo-relative path (following symlinks) and returns it only if the real path is a regular file that stays inside the resolved repo root. It rejects absolute paths, .. traversal, symlink escapes outside the repo, NUL bytes, symlink loops, and non-files by returning None rather than raising, so callers can treat any citation or file reference uniformly.

Sources: src/repodocs/_util.py:L77-L90

LLM backend subprocess execution boundary

run_llm() in backend.py dispatches each generation/planning prompt to exactly one configured backend CLI (omp, claude, or codex), selected via backend_name(), and each invocation is wrapped in a REPODOCS_TIMEOUT-bounded subprocess.run call (default 600s). Every backend receives a system-prompt "contract" from backend_contract(), built from the vendored AGENTS.md plus a mode-specific file (wiki-planner.md or wiki-writer.md), instructing the model to emit only the requested output and never follow instructions embedded in repository content.

Sources: src/repodocs/backend.py:L53-L75, src/repodocs/backend.py:L120-L150

Backend-specific isolation differs materially:

Backend Sandbox mechanism Read boundary
omp --no-rules --no-skills --no-extensions --tools=read,grep,glob, isolation config file Tool allowlist restricts to read/grep/glob
claude --safe-mode --no-session-persistence --permission-mode dontAsk --tools Read,Grep,Glob Tool allowlist restricts to read/grep/glob
codex --sandbox read-only, run against a symlinked repo_view inside a throwaway tempdir Writes only — reads are not restricted

The codex path is explicitly called out as weaker: _warn_codex_read_boundary() documents that --sandbox read-only blocks writes but not reads, so adversarial repository content (a hostile README or source comment) could still cause the model to read and attempt to exfiltrate files outside the repo through generated docs. This warning is printed once per process via a module-level flag, and the code states a real fix would require container/VM-level read isolation, which is out of scope for this CLI.

Sources: src/repodocs/backend.py:L97-L179

flowchart TD
    A[Untrusted repo content] -->|prompt + contract| B{Backend}
    B -->|omp| C[tool allowlist: read/grep/glob]
    B -->|claude| D[safe-mode, tool allowlist]
    B -->|codex| E["--sandbox read-only (write-only boundary)"]
    C --> F[Generated Markdown]
    D --> F
    E --> F
    F --> G[Citation + secret scan before publish]
Loading

Path safety for citations

Generated pages cite source files with repo-relative paths and line ranges. Because those citations are themselves derived from (potentially adversarial) repository content and model output, safe_repo_file() is the shared choke point used to validate that any citation path actually resolves inside the repository before it is trusted — the same function described above under repository scanning.

Sources: src/repodocs/_util.py:L77-L90

Publishing boundary

cmd_publish() and cmd_publish_wiki() in publish.py gate every path from generated output to a public GitHub destination:

  • Protected branch refusal. publish_branch_safe() checks the target branch (after stripping a refs/heads/ prefix and lowercasing) against PROTECTED_PUBLISH_BRANCHES = {"main", "master", "trunk"}; cmd_publish calls die() if the branch is one of these, so a direct publish can never force-push over the repository's primary branch.

    Sources: src/repodocs/publish.py:L116-L166

  • Citation enforcement. cmd_publish computes citation_problems() over the publishable Markdown pages (_publishable_subdir_mds(), which includes this wiki's pages and finished translated subdirs) and blocks publishing if any citation is missing or resolves outside the repository. cmd_publish_wiki calls enforce_citations() similarly before staging is trusted.

    Sources: src/repodocs/publish.py:L146-L178, src/repodocs/publish.py:L377-L386

  • Secret scanning. staged_secret_findings() walks every staged file and matches each line against PUBLISH_SECRET_PATTERNS — PEM-style private key headers, GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_/github_pat_), and cloud/API key shapes (AWS AKIA..., sk-...). It returns only (path, line, label) tuples and deliberately never echoes the matched value, so a false positive can't itself leak a secret into logs. Both cmd_publish and cmd_publish_wiki call this after staging and before any push, die()-ing with the finding list (capped at 10) if anything matches.

    Sources: src/repodocs/publish.py:L119-L139, src/repodocs/publish.py:L182-L185, src/repodocs/publish.py:L383-L386

  • Explicit opt-in for public pushes. Both publish paths require --allow-public; without it, cmd_publish/cmd_publish_wiki print the dry-run staged file list and die() rather than pushing, so a first run is always inspectable before anything reaches the network.

    Sources: src/repodocs/publish.py:L187-L197, src/repodocs/publish.py:L387-L394

  • Symlink rejection during staging. stage_publish(), stage_wiki(), and _reject_symlink_dest() each refuse to read through or write through a symlinked path component, raising ValueError — preventing a symlink placed in the generated output (or in a cloned wiki) from redirecting a read or write outside the intended staging/clone directory.

    Sources: src/repodocs/publish.py:L36-L78, src/repodocs/publish.py:L213-L263, src/repodocs/publish.py:L296-L305

  • Isolated, non-destructive git operations. publish_push() (for the gh-pages-style publish) performs its work in a throwaway detached git worktree under a fresh UUID-scoped branch name, and force-pushes only that staged orphan commit to the remote target branch — it never touches the user's actual working tree or checked-out branch. publish_wiki_push() clones the wiki repo into a tempdir, overwrites only the staged filenames (leaving any other manually-added wiki pages untouched), commits only if the tree changed, and pushes normally (never force).

    Sources: src/repodocs/publish.py:L81-L113, src/repodocs/publish.py:L308-L345

  • Bounded, credential-safe error surfaces. _wiki_git() raises WikiPublishError with only bounded stderr/stdout (via failure_detail(), capped at 200 characters) rather than the raw argv, since a remote URL embedded in the command could carry an embedded credential.

    Sources: src/repodocs/publish.py:L277-L284, src/repodocs/backend.py:L93-L94

Scope not covered by these files

SECURITY.md also states that RepoDocs sanitizes rendered Markdown in the offline viewer with DOMPurify (so repository-derived content cannot inject scripts) and ships third-party license notices alongside vendored assets, and that it does not copy or bundle agent credentials during setup — those behaviors are outside the candidate files for this page and are not further detailed here.

Sources: SECURITY.md:L22-L23

Clone this wiki locally