Documentation menu

Configuration

locac reads one JSON file. Everything in it is optional; CLI flags override it; and it never comes from the repository under audit.

Where the file lives

Path
Default~/.locac/config.json
With LOCAC_HOME set$LOCAC_HOME/config.json
Explicit--config <path>
locac config path     # print the resolved path
locac config init     # write a template there (never overwrites)
locac config show     # print the effective settings for this directory

LOCAC_HOME also holds the per-project session databases (projects/), installed skills (skills/), keybindings, and on Windows the extracted sandbox broker.

This path is a security boundary. The repository under audit is treated as attacker-authored. A config read from it could choose your provider, your base URL, and your API key, so locac never looks there: not for config, not for skills, not for roles, not for prompt text.

Precedence

For each individual field, the last layer that sets it wins:

built-in default  →  global config  →  matching `projects` entry  →  CLI flag

The project entry is chosen by path: an exact match on the resolved --cwd, otherwise the deepest configured ancestor, so a subdirectory inherits its repository's settings. Paths are compared resolved and, on Windows only where the filesystem is case-insensitive, case-folded.

If the working directory matches no project entry and the global config has no model, locac falls back to the most recently added project entry. That is what makes a bare locac in a fresh directory still pick up the model you last configured.

Two extra rules apply to the model source, because a stale API key paired with the wrong endpoint is the one configuration error that leaks a secret to a third party:

  • Naming a provider on the command line clears an inherited customProvider, and vice versa.
  • An API key is only inherited from a layer if that layer did not name a different provider beside it. A key saved next to "provider": "zai" will never be sent to an Anthropic endpoint.

Top-level fields

{
  "provider": "anthropic",
  "model": "claude-opus-4-8",
  "apiKey": "$ANTHROPIC_API_KEY",
  "budget": { "maxTokens": 500000 }
}
FieldTypeNotes
providerstringA built-in provider name: anthropic, openai, groq, ollama, … See Providers.
customProviderstringName of an entry in providers[]. Mutually exclusive with provider.
apistringWire protocol: anthropic-messages, openai-completions, ollama-chat. Only needed for a generic endpoint.
modelstringModel id. Required for a run, but may come from a project entry instead.
baseUrlstringEndpoint base URL for the generic path.
apiKeystringA literal key, or "$VAR" / "${VAR}" to read one from the environment.
authScheme"x-api-key" | "bearer"anthropic-messages only.
systemPromptstringReplaces the default vulnerability-research system prompt.
appendSystemPromptstringAppends to whichever prompt is in force, a role's included, so operator guidance survives a subagent spawn.
outputStylestringProse style for the model's own text. Built-in: direct. Off unless set.
dbstringSession database path. Defaults to a per-project path under LOCAC_HOME.
shellPathstringPath to a bash-compatible shell for exec and the sandbox. Auto-discovered when unset.
compactionobjectSee Compaction.
budgetobjectSee Budget.
providersarraySaved custom endpoints. See Custom providers.
modelsobjectPer-model settings keyed by model id. See Per-model settings.
projectsobjectPer-repository overrides keyed by absolute path. See Per-project overrides.
webobjectDashboard bind settings. See Dashboard.
authobjectDashboard credentials. See Dashboard.
rolesobjectSubagent role overrides. See Roles.

Unknown keys are discarded, never persisted.

API keys

Three ways to supply one, in increasing order of how much you are trusting the file:

  1. Environment variable only. Leave apiKey unset and export the provider's variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, …). locac reads it at run time.
  2. Reference from config. "apiKey": "$ANTHROPIC_API_KEY". Both $VAR and ${VAR} are expanded through a single choke point. An unset variable resolves to nothing and the run fails loudly, because locac never sends an empty key.
  3. Literal in config. "apiKey": "sk-...". config init writes the file with tightened permissions precisely because this is the next thing an operator does.

However you supply it, the key never reaches the browser: the dashboard is served a redaction sentinel, and every key present anywhere in the config, whether global, per-provider or per-project, is registered with the output guard so it is scrubbed from tool output even for runs that did not select it.

Budget

Caps that stop a run before it stops itself. A run halted by a cap exits with code 2.

{
  "budget": {
    "maxIterations": 0,
    "maxToolCalls": 400,
    "maxTokens": 500000,
    "maxWallClockMs": 3600000
  }
}
FieldMeaning
maxIterationsAgent turns. 0 or unset means unlimited.
maxToolCallsTotal tool invocations across the run.
maxTokensTotal tokens.
maxWallClockMsWall-clock milliseconds.

Each has a CLI equivalent: --max-iterations, --max-tool-calls, --max-tokens, --max-wall-clock-ms. All four must be positive integers on the command line, so 8k and 80.5 are rejected rather than silently coerced.

Compaction

Context shrinking is on by default and is deterministic: a rule-based elision pass, not an LLM summarisation call.

{
  "compaction": {
    "enabled": true,
    "mode": "elide",
    "reserveTokens": 40000,
    "keepRecent": 8,
    "evidenceBudgetTokens": 20000
  }
}
FieldDefaultMeaning
enabledtrueSet false to disable. A long run will then overflow the context window.
mode"elide"elide is the LLM-free default. summarize is reserved.
reserveTokens20% of the windowHeadroom left for the response.
keepRecent8Trailing messages kept verbatim.
evidenceBudgetTokenshalf the reserveBudget for old sink / finding / PoC evidence.

Elision is non-destructive: the full body stays in the transcript store, and an elided result renders as [elided <n> chars of <tool> output — recall seq=<N>], which the agent can restore verbatim with the recall tool instead of re-running an expensive command. Full details on the Context & compaction page.

Custom providers

Any endpoint that speaks one of the supported wire protocols. Saved by name, then selected with --custom-provider <name> or the config's customProvider field.

{
  "providers": [
    {
      "name": "corp-gateway",
      "api": "openai-completions",
      "baseUrl": "https://llm.corp.internal/v1",
      "apiKey": "$CORP_LLM_KEY",
      "models": [
        {
          "id": "corp-large",
          "contextWindow": 262144,
          "maxTokens": 32000,
          "reasoning": true,
          "reasoningEffort": "high",
          "finishReasonOptional": true,
          "cost": { "input": 3, "output": 15, "cacheRead": 0.3, "cacheWrite": 3.75 }
        }
      ]
    }
  ],
  "customProvider": "corp-gateway",
  "model": "corp-large"
}

Provider fields

FieldNotes
nameSelection key and display name. Letters, digits, _, -.
apianthropic-messages, openai-completions, or openai-responses.
baseUrlMust already include the version segment the protocol appends onto, e.g. .../v1.
apiKeyLiteral or $VAR.
authSchemex-api-key or bearer. anthropic-messages only.
models[]The models this endpoint serves.

Model fields

FieldNotes
idModel id sent on the wire.
contextWindowTokens. Set this. locac cannot infer it for a custom endpoint, and an over-claimed window is the one error that kills a run: compaction believes it has headroom it does not, never fires, and the provider rejects the request.
maxTokensPer-response output cap.
reasoningWhether the model is a reasoning model.
reasoningEffortSent as reasoning_effort on openai-completions. This field takes any token your endpoint accepts, including spellings the --effort menu never offers, like minimal, xhigh or max.
finishReasonOptionalSet when the endpoint never sends finish_reason, so its absence is not read as a truncated stream.
cost{ input, output, cacheRead, cacheWrite } per million tokens, for the run's cost accounting. Defaults to all-zero.

Per-model settings

For built-in providers you usually only need to override the context window. Keyed by model id:

{
  "models": {
    "claude-opus-4-8": { "contextWindow": 1000000 }
  }
}

A custom provider's own model entry is more specific and wins over this map.

Per-project overrides

Keyed by the absolute, resolved path of the repository root:

{
  "projects": {
    "/home/me/audits/acme-api": {
      "provider": "anthropic",
      "model": "claude-opus-4-8",
      "appendSystemPrompt": "This service is behind an authenticating gateway; treat only the /public prefix as unauthenticated.",
      "budget": { "maxToolCalls": 600 },
      "shellPath": "/usr/bin/bash"
    }
  }
}

A project entry may set: provider, customProvider, model, api, baseUrl, apiKey, authScheme, systemPrompt, appendSystemPrompt, budget, shellPath.

appendSystemPrompt is the field to reach for when you want per-repository guidance. Unlike systemPrompt it does not replace the vulnerability-research prompt, and it survives a subagent spawn: a role's prompt replaces the default, but your appended text is still joined onto it.

Session database

The database holds the transcript, the evidence artifacts, and the cross-verify verdicts. It lives per project, under LOCAC_HOME:

~/.locac/projects/<basename>-<sha256-prefix>/sessions.db

The directory name is derived deterministically from the resolved root: a readable basename so you can find your own sessions, plus a twelve-character hash that does the actual disambiguation.

It is outside the target repository by design. Sandboxed target code has write access to the target root, and that database is the trust anchor every High/Critical gate reads. A single inserted verdict row would let the code under audit satisfy the quorum that exists to judge it. It would also place the full transcript inside a tree the target's own build and CI can read.

--db <path> or the db config field overrides the location, which is also the migration path for an older in-repository database.

Dashboard

{
  "web": { "host": "127.0.0.1", "port": 4173 },
  "auth": {
    "passwordHash": "$argon2id$...",
    "jwtSecret": "..."
  }
}
FieldDefaultNotes
web.host127.0.0.1Loopback only.
web.port4173
auth.passwordHashnoneargon2 hash of the dashboard password.
auth.jwtSecretnoneHS256 signing secret for the session cookie.

Binding a non-loopback host without both auth fields is refused, fail-closed. The dashboard can launch bash; it is never exposed unauthenticated. Both auth fields are secrets and are redacted before anything reaches the browser.

--host and --port on locac web override the config block.

Roles

Subagents are spawned as typed roles, each carrying its own system prompt, tool allow-list, and task/report schemas. Built-in roles live in code; this map overrides their tunable fields or defines new ones.

{
  "roles": {
    "exploit-verifier": {
      "model": "claude-opus-4-8"
    },
    "docs-reader": {
      "description": "Read the vendor's changelog and advisories for the version under test.",
      "systemPrompt": "You read documentation only. Never modify files.",
      "tools": ["read", "search", "find", "ls", "submit_report"]
    }
  }
}
FieldNotes
descriptionOne line telling the orchestrator when to spawn this role.
systemPromptReplaces the default prompt for the child.
toolsAllow-list of tool names, validated against the registry at load.
modelPer-role model id. Unset means the child inherits the parent's model.

A role defined here that does not match a built-in name gets generic task and report schemas, because zod schemas cannot come from JSON. See the Skills & roles page.

Roles are trusted input, which is exactly why they are only ever read from LOCAC_HOME. A role is a system prompt plus a tool allow-list; letting the repository under audit define one would let it rewrite the instructions of the agent auditing it.

DAST authenticated scans

dast.auth supplies static credentials so a DAST run can probe behind a login. Credentials are attached only to a request whose host is in hosts - an off-host redirect or SSRF target never receives them - and a request's own header wins over a configured default.

{
  "dast": {
    "auth": {
      "hosts": ["api.example.com", "app.example.com"],
      "headers": { "Authorization": "Bearer eyJhbGciOi..." },
      "cookies": { "session": "s%3A9xI..." }
    }
  }
}
FieldNotes
hostsOnly a request bound for one of these hosts receives the credentials. Required.
headersStatic request headers (e.g. a bearer token) attached to same-host requests.
cookiesCookie name/value pairs attached to same-host requests.

Like roles and API keys, dast.auth is trusted input read only from LOCAC_HOME, never from the repository under audit. Every header and cookie value is registered as an operator secret, so the output guard scrubs it from probe evidence and the transcript before either reaches the model.

Full example

{
  "provider": "anthropic",
  "model": "claude-opus-4-8",
  "apiKey": "$ANTHROPIC_API_KEY",
  "appendSystemPrompt": "Prefer memory-safety and injection classes; skip client-side issues.",
  "shellPath": "C:\\Program Files\\Git\\bin\\bash.exe",
  "budget": { "maxTokens": 2000000, "maxWallClockMs": 7200000 },
  "compaction": { "keepRecent": 12 },
  "models": { "claude-opus-4-8": { "contextWindow": 1000000 } },
  "projects": {
    "D:\\audits\\acme-api": {
      "model": "claude-opus-4-8",
      "budget": { "maxToolCalls": 800 }
    }
  },
  "web": { "port": 4173 }
}

Next

The Flag reference lists every flag referenced above, with its default and the validation rules that apply. Providers and Environment variables break down every endpoint and variable.