configuration.md

August 10, 2026 · View on GitHub

Configuration

This page documents .openlore/config.json. For the LLM provider table (keys, env vars, default models), see providers.md. To list opt-in features and how to enable each, run openlore features.

openlore init creates .openlore/config.json:

{
  "version": "1.0.0",
  "projectType": "nodejs",
  "openspecPath": "./openspec",
  "analysis": {
    "maxFiles": 500,
    "includePatterns": [],
    "excludePatterns": []
  },
  "generation": {
    "model": "claude-sonnet-4-20250514",
    "domains": "auto"
  }
}

Environment Variables

VariableProviderDescription
ANTHROPIC_API_KEYanthropicAnthropic API key
ANTHROPIC_API_BASEanthropicCustom base URL (proxy / self-hosted)
OPENAI_API_KEYopenaiOpenAI API key
OPENAI_API_BASEopenaiCustom base URL (Azure, proxy...)
OPENAI_COMPAT_API_KEYopenai-compatAPI key for OpenAI-compatible server
OPENAI_COMPAT_BASE_URLopenai-compatBase URL, e.g. https://api.mistral.ai/v1
GEMINI_API_KEYgeminiGoogle Gemini API key
COPILOT_API_BASE_URLcopilotBase URL of the copilot-api proxy (default: http://localhost:4141/v1)
COPILOT_API_KEYcopilotAPI key if the proxy requires auth (default: copilot)
EMBED_BASE_URLembedding (remote)Base URL for a remote OpenAI-compatible embedding API (e.g. http://localhost:11434/v1)
EMBED_MODELembedding (remote)Remote embedding model name (e.g. nomic-embed-text)
EMBED_API_KEYembedding (remote)API key for the remote embedding service (defaults to OPENAI_API_KEY)
DEBUG--Enable stack traces on errors
CI--Auto-detected; enables timestamps in output
OPENLORE_NO_AUTO_ANALYZE--Disable the MCP server's cold-start self-bootstrap (no background index build on first run)
OPENLORE_NO_UPDATE_NOTIFIER--Silence the passive "update available" banner (NO_UPDATE_NOTIFIER is also honored)
OPENLORE_SKIP_POSTINSTALL--Suppress the post-install next-step hint
OPENLORE_NO_WORKERSanalyzeRun per-file extraction on a single thread instead of the worker pool. Both lanes produce byte-identical analysis output, so this only costs wall-clock — set it to isolate a worker-related problem, or in an environment where extra threads are unwelcome
OPENLORE_NO_FACT_CACHEanalyzeRe-extract every file instead of reusing the per-file extraction cache, exactly as --force does (and, like --force, the cache is refilled afterwards). Both lanes produce byte-identical analysis output, so this only costs wall-clock — reach for it when a CLI flag is not available, e.g. from an embedded caller
OPENLORE_NO_AUTO_HEAPCLIDisable adaptive heap sizing — the CLI runs at Node's default heap (or whatever you set) with no re-exec, exactly as before this feature existed
OPENLORE_HEAP_MBCLIForce the analyzer's V8 old-space heap to this many MB (skips auto-detection). Equivalent to setting --max-old-space-size yourself, but as an env knob
OPENLORE_HEAP_FRACTIONCLIFraction of available memory used for the heap when sizing automatically (default 0.75)
OPENLORE_FORCE_MEMORY_TIERanalyzeForce the graceful-degradation tier: full, shed-overlay, or shed-overlay-and-deep-analysis. Overrides the pre-flight estimate — for a memory-constrained CI job that wants reduced fidelity deterministically, or to reproduce the degraded path

The extraction cache costs disk. analyze memoizes each file's extracted facts inside call-graph.db, keyed by content hash, so a later run re-parses only what changed. It is the largest table in the store — roughly 10 MB per 800 source files (about +55% on the graph index). It is a pure cache: deleting .openlore/analysis/ reclaims it and costs only time, never correctness. (analyze --force re-extracts everything and then refills the cache, so it reclaims nothing — it is the correctness escape, not the disk one.) The cache is stripped from openlore export bundles, which carry the graph and not this machine's build cache.

The EMBED_* variables configure the remote embedding provider only. For on-device embeddings with no endpoint or key, run openlore embed --local (or set embedding.provider: "local" in .openlore/config.json). Keyword (BM25) search is the first-class default and needs none of these. See docs/semantic-search.md for the full embedding/retrieval-mode reference.

Repository secret redaction

OpenLore redacts credential-shaped repository content before source-carrying tool results reach an agent. Redacted results include a count and the matched credential kinds. Persisted LLM request logs use the same pattern set for both prompts and responses and record the redaction count per request.

The tool-output boundary is enabled by default. A trusted solo operator who requires byte-exact tool output can opt out explicitly:

{
  "secretRedaction": {
    "toolOutput": false
  }
}

This opt-out does not disable redaction in LLM logs, errors, or telemetry.

Analyzing at any repository size

The promise: openlore analyze works to your machine's capacity, degrades gracefully and transparently beyond it, and never crashes — with no flags and no attention. Two mechanisms deliver that, both on by default:

  1. Adaptive heap sizing. A very large repository's call graph can outgrow Node's default V8 heap, and the old recourse was knowing to pass --max-old-space-size by hand. The CLI now sizes its own heap: the finite graph-building commands (analyze, install, prove, import, run) re-execute themselves once at startup with a heap set to a generous fraction (0.75) of the memory available to the process — the container/cgroup limit when there is one (walking the cgroup hierarchy for the tightest cap, so a CI or container job sizes to its own limit, not the host's, and is not OOM-killed), otherwise host RAM. It is quiet and safe: at most once (a marker prevents any loop), skipped when you already set the heap (--max-old-space-size / NODE_OPTIONS) or opted out (OPENLORE_NO_AUTO_HEAP), and the one-line disclosure goes to stderr, never stdout. The query hot paths (orient, search, …) and the long-lived daemons (mcp, serve) are not re-executed — the hot paths read bounded data and must not pay an extra process spawn, and a daemon must not run behind a blocking supervisor that could orphan it on a directed signal; give a daemon on a huge repo a heap the ordinary way (--max-old-space-size or OPENLORE_HEAP_MB, both honored). Turn sizing off with OPENLORE_NO_AUTO_HEAP=1; pin an exact size with OPENLORE_HEAP_MB; change the fraction with OPENLORE_HEAP_FRACTION.

  2. Graceful degradation, disclosed. Before the heavy passes, the analyzer estimates the memory the graph will need from the repository's own size (file count + bytes). If even the largest sane heap will not hold full-fidelity analysis, it sheds the most expensive, least-essential work first — the CFG/def-use overlay, then LLM deep-analysis breadth — and still produces a usable index (call graph + search intact), rather than aborting with a raw out-of-memory fatal. Whatever it reduced is disclosed in parse-health.json (a memoryDegradation record) and in one CLI line, so a downstream conclusion reads reduced coverage as reduced, never as genuine structural absence.

Determinism is preserved. Heap size and buffer-versus-spill choices never change the produced artifact: two full-fidelity runs of the same repository are byte-identical regardless of how much RAM each machine had. Only the degradation ladder reduces content, and only as a function of declared constraints (available memory, repository size) — disclosed and reproducible, never a silent machine-dependent difference between two "full" runs.

Embeddable API path. The in-process API (import { analyze } from 'openlore') cannot re-execute the host process, so it does not resize the heap — the host owns that. The degradation ladder still applies within whatever heap the host provides, so an over-capacity repository degrades gracefully there too instead of crashing. Because the pre-flight estimate is deliberately conservative (it over-estimates so degradation triggers before a crash), an embedding host running at Node's default heap may shed the overlay on a large-but-manageable repository. If you want full fidelity there, give the host process a real heap (--max-old-space-size, or OPENLORE_HEAP_MB), or force the tier with OPENLORE_FORCE_MEMORY_TIER=full when you know it fits.

Not yet in scope: an out-of-core (streaming) graph — a graph larger than RAM, persisted and never held whole. Adaptive heap plus graceful degradation cover the practical need; a genuine RAM-ceiling case at normal function density would earn its own proposal.

Spec-store binding

An optional specStore block in .openlore/config.json binds this repository to an external spec store — a standalone repository that holds specs/changes — and declares the code repositories its plans are about. It is configuration only: OpenLore reads the declared relationships and never clones, writes to, syncs, or fences the store or any target. Omit the block entirely for unchanged single-repository behavior.

{
  "specStore": {
    "name": "team-plans",
    "path": "../team-plans",
    "targets": ["api", "web"],
    "references": ["design-system"]
  }
}
FieldRequiredMeaning
nameyesa stable, user-facing name for the store
pathyesabsolute or repo-relative path to the external spec repository
targetsyesfederation-registered names of the code repositories the store's work is about
referencesnofederation-registered names of repositories the store draws on for context

targets and references are names, not paths: each must match a repository registered with openlore federation add … --name <name> (see Federation). Check the binding's health with openlore spec-store status (CLI reference); it reports per-target resolution, index freshness, reference presence, and store-path presence as findings with stable codes, and never blocks.

Covering surfaces (change-impact certificate)

An optional impactCertificate block declares the covering surfaces the change-impact certificate assesses a diff against — semantic or governance boundaries (a client surface, a data-handling surface, a regulated interface), not directory globs. Omit it entirely and the certificate still reports blast radius, drifted specs, and tests; declaring surfaces additionally reports the paths a change newly opens into each. See openlore impact-certificate and the change_impact_certificate MCP tool.

{
  "impactCertificate": {
    "surfaces": [
      {
        "name": "client",
        "severity": "critical",
        "members": [
          { "symbol": "renderResponse" },
          { "file": "src/api/public.ts" }
        ]
      }
    ],
    "block": ["critical"]
  }
}
FieldRequiredMeaning
surfaces[].nameyesa stable, user-facing surface name (must be unique; empty names and duplicates are dropped)
surfaces[].membersyesthe boundary's members: each is a { "symbol": "<name>" } (resolved to exactly one indexed symbol — ambiguous/unknown becomes a finding, never guessed) and/or a { "file": "<repo-relative path>" } (all of the file's symbols). A member may set both.
surfaces[].severitynoinfo | warn | critical (default warn); any other value is coerced to warn
blocknoseverities the advisory git hook should fail a commit on (e.g. ["critical"]). Empty/absent = advisory-only (the default). Infrastructure failure never blocks. Now thin legacy sugar that lowers onto enforcement.policy (["critical"]{ "surface-critical": "blocking" }); a direct policy entry wins.

A surface is resolved against the indexed graph (plus any symbol the same diff just added). The certificate is advisory by default and decays via the code-anchored freshness lease; when an anchored symbol later moves, openlore spec-store status re-fires a persisted certificate as a certificate-stale finding.

Enforcement policy

An optional enforcement.policy block is the single source of truth for what blocks a commit, what merely advises, and what is deliberately silenced. It maps a stable governance finding code to one enforcement class — blocking, advisory, or off — decoupling a finding's intrinsic severity (owned by the source that computes it) from this repository's risk posture (owned here). It is consumed by openlore enforce, the unified gate.

{
  "enforcement": {
    "policy": {
      "stale-decision-reference": "blocking",
      "surface-critical": "blocking",
      "orphans-anchored-memory": "off"
    }
  }
}
  • Additive and optional. An absent or empty policy preserves today's behavior exactly — every finding stays advisory by default, so nothing newly blocks.
  • Deterministic precedence. A finding's class is a pure function of (code, policy): an explicit off wins over an explicit blocking, which wins over an explicit advisory, which wins over the source-declared default. Resolution is order-independent.
  • Severity is never changed. The policy decides enforcement class only; the emitting source remains the sole authority on a finding's intrinsic severity.
  • off is visible, not invisible. A silenced finding is still listed in the gate output (marked off), so a deliberate silence is auditable.
  • Legacy block sugar lowers onto it. blastRadius.block: ["orphans-anchored-decision"] and impactCertificate.block: ["critical"] are thin equivalents of enforcement.policy: { "orphans-anchored-decision": "blocking" } and { "surface-critical": "blocking" }. A direct enforcement.policy entry always wins over inherited legacy sugar.
  • Unknown codes are retained. Naming a code no installed source emits yet is not an error — the entry is kept and surfaced as an informational note, so a policy may name a code before its source ships.

The governable finding codes (the finding-code catalogue — every code defaults to advisory; blocking is always opt-in):

CodeSourceDefaultMeaning
stale-decision-referencestale-decision-referenceadvisoryA live, authoritative artifact (approved decision, non-orphaned anchored memory, or spec requirement) references a decision that has since been superseded.
orphans-anchored-memoryblast-radiusadvisoryThe change orphans one or more code-anchored memories.
orphans-anchored-decisionblast-radiusadvisoryThe change orphans one or more anchored architectural decisions.
surface-infoimpact-certificateadvisoryThe change opens a new path into a declared covering surface marked info.
surface-warnimpact-certificateadvisoryThe change opens a new path into a declared covering surface marked warn.
surface-criticalimpact-certificateadvisoryThe change opens a new path into a declared covering surface marked critical.
parallel-work-conflictplan-parallel-workadvisoryTwo tasks proposed for concurrent work have a write-write (WAW) conflict; plan_parallel_work schedules them into different waves.
parallel-work-cycleplan-parallel-workadvisoryA set of proposed tasks forms an unorderable read-after-write cycle; plan_parallel_work schedules the members mutually exclusive and the circular dependency should be resolved.
cross-actor-conflictinterference-mapadvisoryTwo in-flight changes (branches/PRs/agent tasks, within or across a federation) have a write-write (WAW) conflict on a shared symbol; map_in_flight_conflicts reports them as must-not-land-concurrently. A CI check can name this code to warn when a new PR collides with an open one.

Note on the surface codes. The change-impact certificate's own --json finding codes are surface-newly-reached / surface-critical (see mcp-tools.md); the enforcement gate governs the per-severity codes surface-info / surface-warn / surface-critical (one per declared surface severity). To block a surface via enforcement.policy, name the per-severity code (e.g. "surface-critical": "blocking"), not surface-newly-reached — an unrecognized code is retained but governs nothing.

Note on the parallel-work / cross-actor codes. parallel-work-conflict / parallel-work-cycle (from plan_parallel_work) and cross-actor-conflict (from map_in_flight_conflicts) are emitted only by those MCP tools, which openlore enforce never runs (the gate is diff-based; these tools need a caller-supplied task list or live git/PR state). Naming them in enforcement.policy lets the caller that invokes the tool classify its findings[] with resolveEnforcementClass and block in its own CI — e.g. a CI check that warns when a new PR's footprint collides with an open one; the bundled commit gate never produces or blocks on them.

Task-scoped context injection

An optional contextInjection block controls the per-task orientation that openlore install wires as a Claude Code UserPromptSubmit hook (openlore orient --inject). It runs orient against your submitted prompt and places a bounded, ignorable orientation block in context before the agent's first turn, so the common task begins already oriented without a manual orient call — amortizing the per-task round-trip the Value Scorecard attributes the small/familiar loss case to. Omit the block entirely for the defaults below (injection enabled). See openlore install.

{
  "contextInjection": {
    "mode": "task-scoped",
    "tokenBudget": 600,
    "relevanceMinMatches": 2,
    "relevanceMinFanIn": 2,
    "relevanceMinScore": 0.3
  }
}
FieldDefaultMeaning
modetask-scopedtask-scoped enables injection; off makes orient --inject a no-op (exit 0). Disabling does not affect the MCP server or the SessionStart primer.
tokenBudget600Hard cap on the injected block, in estimated tokens, with a 68-token minimum for the mandatory framed task and stale-index disclosure. Lower positive values are clamped to 68; lower-priority detail (functions → files → call neighbours → specs → tools) is dropped to stay within budget.
relevanceMinMatches2Relevance gate: minimum matched-function count unless the prompt names a matched identifier exactly (below it → a one-line pointer).
relevanceMinFanIn2Relevance gate: a match with at least this fan-in (or a hub) clears the gate structurally. Exact identifier mentions and scale-free top-match identifier overlap can also clear the gate.
relevanceMinScore0.3Relevance gate: minimum top match score — used only on the bounded semantic/hybrid score scale (BM25-fallback scores are corpus-relative and the score path is disabled there).

The relevance gate is deterministic and never learned: when a task's graph match is weak (the small/familiar/shallow case), injection degrades to a single pointer line rather than taxing a task that needs no orientation. Injection is fail-open — any failure (no graph, parse error, empty/weak match) emits the pointer line and exits 0, so the hook can never break the agent's turn. Set OPENLORE_INJECT_DEBUG=1 to write the gate verdict and applicable failed criteria to stderr; stdout remains injection-only.

Without embeddings (the default keyword/BM25 index) the gate never compares the unbounded, corpus-relative BM25 score to a fixed threshold. It uses matched-function count plus scale-free evidence: an exact identifier mention, top-match identifier-token overlap, or fan-in/hub centrality. Running openlore embed --local (one command, on-device, no API key) — or configuring a remote EMBED_* endpoint — additionally enables the bounded semantic-score path (relevanceMinScore). The injected block is always explicitly ignorable, so a false positive costs only a few tokens.