031 · Project configuration
Project configuration with a trust boundary (spec_project_config_031)
| Status | Implemented (2026-09-29), from the design reviewed that day; §10 records the decisions taken. It details spec_parity_027 §2.4 (PAR-CFG-01–06) under decision §12.3. |
| Source | pkg/config/{project,trust}.go; pkg/engine/project.go; pkg/engine/skills/policy.go (untrusted roots); pkg/engine/tools/permrules.go (SourceProject); apps/service/internal/server/workspace.go; pkg/client/project.go; apps/cli/{setup,trust,doctor}.go; apps/cli/internal/tui/project.go; apps/desktop/web/src/{ProjectDialog.tsx,project.ts} |
| Tests | pkg/config/project_test.go, pkg/engine/project_test.go, pkg/client/project_test.go, apps/cli/trust_test.go, apps/cli/internal/tui/project_test.go, apps/desktop/web/src/project.test.ts |
| Depends on | spec_config_002 (CFG-01, the workspace overlay CFG-40/41), spec_approvals_005 (permission rules and scopes), spec_workers_023 (hash-pinned review, WK-30), spec_service_021, spec_desktop_024 |
| Size | L, built in the phases of §9 |
A team should be able to commit what Blitz needs to work in its repository (hooks, MCP servers, slash commands, permission presets) so that everyone gets it by cloning. That is also how a hostile repository attacks the person who opens it. Claude Code’s project settings led to three CVEs: hooks ran before the trust dialog (CVE-2025-59536), a repository redirected the API base URL and received the user’s key (CVE-2026-21852), and a crafted repository reused another directory’s trust (CVE-2026-40068).
This design lets a repository carry settings in three tiers:
- Tighten-only settings apply at once.
- Settings that run code or loosen policy apply only after the person trusts that exact content.
- Credentials, endpoints and sandbox loosening never apply from a repository.
- Settings files. Settings come only from the user’s configuration directory (CFG-01):
~/.blitz/.env.toml, then the per-workspace overlay~/.blitz/workspaces/<name>-<hash>/.env.toml, then environment variables, then flags. Both files are the user’s own, so either may set any key. The overlay’s permission rules are added to the global ones, and every other key replaces the global value. - Read from the repository without trust:
- instruction files (
AGENTS.md,CLAUDE.md,GEMINI.md,BLITZ.md) and rule directories; - slash commands (
.blitz/commands,.claude/commands,.agents/workflows); - worker definitions, which are hash-pinned (WK-30).
- instruction files (
- Read from the repository only with
blitz.trust_workspaceor--trust-workspace: agents (.agents/agents;./agentsuntil 2026-10-01) and skills (./skills,.agents/skills). Attached to the service,--trust-workspaceis lost (client.Attachsends only the directory). - Nothing asks the user outside a turn. The service’s broker only asks during a running turn. The closest pattern is the worker review: list the item with its hash, then enable that hash; the service refuses a hash that has changed (
HASH_MISMATCH).
- PRJ-01 A workspace may have
.blitz/settings.toml(shared, committed) and.blitz/settings.local.toml(personal). The loader warns when the local file is tracked by git, as it does for local memory files (memory.TrackedByGit). - PRJ-02 Both files must be regular files: a symlink is ignored with a warning. Each may be at most 256 KB, and is decoded as TOML and nothing else. No include,
keychain:orxor:resolution and no environment expansion is applied to their values. - PRJ-03 A project file uses the same key names as
.env.toml, but only the keys in §4 are accepted. Any other key is ignored with a warning that names the file and the key. The list says what a project may set; nothing is inferred.
| Tier | Keys | Applied |
|---|---|---|
| A: informs or tightens | permissions.deny, permissions.ask; sandbox.blocked_paths (additions); tools.max_parallel, tools.shell_timeout_seconds, tools.max_file_size_bytes, skills.policy.max_timeout_seconds, workers.policy.max_turns, workers.policy.max_cost_usd, each only when lower; skills.policy.min_hitl_tier only when higher; skills.policy.deny_tools (additions); plugins.disable (installed plugins off, spec_plugins_033); agent definitions and skills as prompt text (.agents/agents, ./skills, .agents/skills) |
At once, with no question |
| B: runs code or loosens | [[hooks.*]]; [[mcp.servers]] (command and HTTP, without auto_approve or sandbox); permissions.allow; sandbox.shell_writable_paths; blitz.default_model and [agent_models]; workers.policy.allow (added kinds); plugins.enable (installed plugins on); scripts of the project’s skills (run_skill_script) |
Only after trust (§5) |
| C: never | Everything else. With the reason “never from a project”: [llm] (keys, endpoints, providers), [web], [browser], [telemetry], [log], [audit], [checkpoints], [session], images.dir, [pricing], ui.status_line, blitz.auto_approve, blitz.permission_mode, blitz.trust_workspace, [permissions.auto], sandbox.shell, sandbox.share_adc, sandbox.allow_network, sandbox.allowed_paths, sandbox.read_only_paths, sandbox.scrub_env, sandbox.commands, tools.auto_approve_commands and the tools’ directories, mcp.servers.auto_approve, mcp.servers.sandbox, the looser [skills.policy] keys, workers.policy.max_concurrent, workers.paths, skills.paths; any other key as “not a project setting” |
Ignored, with a warning |
- PRJ-10 Tier A, how it merges. Deny and ask rules are added, labelled with the scope
project. Blocked paths are added. A policy value or limit applies only when it is stricter than the user’s; otherwise it is ignored anddoctorsays so. - PRJ-11 Tier B, how it merges. The user’s settings keep precedence (PAR-CFG-01):
- Project hooks run after the user’s hooks for the same event.
- A project MCP server is added unless the user has one with the same name, which wins. It is never auto-approved and always sandboxed.
- Allow rules are added with the scope
project. - Writable paths must lie inside the workspace; any outside it are ignored (“outside the workspace”).
blitz.default_modelapplies only when the user’s settings don’t set one, and an agent’s model only when the user hasn’t pinned that agent (“your own settings set it”). Either applies only for the configured provider or one with an API key (“a provider you haven’t set up”).
- PRJ-12 Precedence. From lowest to highest: defaults,
~/.blitz/.env.toml,.blitz/settings.toml,.blitz/settings.local.toml, the user’s workspace overlay, environment variables, flags. Project deny rules are always added, whatever the precedence.
-
PRJ-20 What is hashed. Trust is keyed by the workspace’s canonical path (absolute, symlinks resolved, with no repository-controlled indirection such as git’s
commondiror worktree pointers) and a hash of the tier B content only. The hash covers:- the tier B keys of both files, normalised to canonical JSON with sorted keys, each tagged with its file;
- the content of every workspace file that a tier B command names as its program (for example
./scripts/lint-hook.sh), so that editing the script asks again; - the content hashes (
ContentHash) of the project’s skills that have scripts.
Editing comments or tier A keys doesn’t ask again.
-
PRJ-21 Where decisions are kept. In
~/.blitz/trust.json, written atomically by the service or by the local CLI, likeworkers.json:{"version": 1, "workspaces": {"/Users/me/src/app": {"hash": "sha256:…", "trusted": true, "at": "2026-09-29T10:00:00Z"}}}A “don’t trust” answer is recorded too (
trusted: false), so the question isn’t repeated until the content changes. The states arenone(no tier B content),new,changed(the hash differs),trustedanddeclined. A damaged file counts as no decisions. -
PRJ-22 When tier B loads. Only when the state is
trusted. Otherwise the workspace opens without it: no hook, MCP server or allow rule from the project is loaded, started or parsed beyond TOML decoding (PAR-CFG-06). The trust check runs inconfig.LoadWorkspace, beforeengine.Openstarts hooks and MCP servers. -
PRJ-23 Trusting is for one hash. It records that hash; the content is read again from disk and an answer about a hash that no longer matches is refused (
api.ErrProjectChanged,PROJECT_CHANGEDover the API), as workers refuse a stale review. In the service the workspace then reopens (TrustProjectreturnsreopened); a workspace opened by the REPL itself picks the decision up when it next opens. -
PRJ-24 What people see before trusting. The exact items, one line each, with what they do: “hook
pre_toolruns./scripts/check.sh(in the repository)”, “MCP serverdbrunsnpx @acme/db-mcp”, “allow ruleBash(make test)”. People trust the items shown, not a file name.
- PRJ-30 The REPL, run locally. Configuration is loaded before
engine.Open. When the state isneworchangedand stdin is a terminal, the REPL lists the items (engine.ReviewProject) and asksTrust them? [t]rust, [d]on't trust, [s]how the files, records the answer, then opens the workspace, so nothing starts twice. The question uses a plain line reader, before the line editor exists. Unanswered (Ctrl+C, end of input) records nothing. - PRJ-31 The REPL attached to the service. Right after attaching,
GetProjectSettingsreturns the state, the items and the hash. If trust is needed, the REPL asks the same question and callsTrustProject(hash, trusted), and the service reopens the workspace. If a turn is running in it (another client’s), the workspace isn’t reopened: the decision applies when it next opens. - PRJ-32 No terminal (
blitz exec, pipes). The project is treated as untrusted and one line on stderr says so.--trust-projecttrusts the content for that run only, without recording anything, in a workspace opened by the CLI; attached to the service it only warns (the service’s workspace is shared:blitz trustrecords a decision).--trust-workspaceis a hidden alias of--trust-project. - PRJ-32a At start. The REPL prints one line when a project has settings: how many apply, and how many wait for trust (a warning then).
- PRJ-33 The desktop app. Opening a workspace calls
GetProjectSettingswith its settings. Forneworchanged, a dialog (ProjectDialog.tsx) lists the files, what needs trust and its state, what applies, and what was ignored with why, with Trust and Don’t trust; it opens once per content. While settings wait, a bar above the conversation says how many (“Project settings not loaded: 3 settings wait for your trust.”) with Review. Settings › Workspaces has Project settings for each open workspace, opening the same dialog. - PRJ-34 Commands.
blitz trust [--revoke] [dir]and/trustshow the state and the items, and change the decision.blitz doctorlists the project files, the tier A settings applied, tier B’s state, and each ignored key with its reason.
- PRJ-35 Workers. A worker’s run uses the workspace as opened. If tier B isn’t trusted, project hooks don’t run for workers either.
-
PRJ-40
api.Backendhas two methods:ProjectSettings() api.ProjectSettings: files, applied items, pending items, ignored keys, state and hash;TrustProject(hash string, trusted bool) error.
In the service these are
WorkspaceService.GetProjectSettingsandTrustProject. The client implements them, so the REPL works the same attached or local. -
PRJ-41 Permission listings (
ListPermissionRules, the run settings panel) show the scopeprojectfor rules from project files. They can’t be removed from Blitz: they’re edited in the file.
- Agents and skills.
.agents/agentsand./skillsload as prompt text withouttrust_workspace(tier A), because PAR-CFG-02 says agent definitions and skills as prompt text load without asking. Their scripts need trust (tier B): until thenskills.Evaluateblocks them (“it’s the project’s (…), whose settings aren’t trusted: blitz trust”). blitz.trust_workspace. Deprecated, with a warning at open. Set in the user’s own settings, it trusts the project’s content for each run without recording anything (like--trust-project); a project can’t set it.- Commands and instructions. They already loaded without trust and stay that way (tier A).
- Loading (
pkg/config): the key table and tiers, merging, the report of applied, pending and ignored items, the hash. Table-driven tests for every key in §4 and for each loosening that must be refused. - Trust store (
~/.blitz/trust.json) and the reopen:ProjectSettings/TrustProjectin the engine, the service and the client, with theHASH_MISMATCHcontract. - CLI: the prompt before open,
--trust-project,blitz trust,/trustand doctor. - Desktop app: the dialog, the bar, and Settings › Workspaces › Project settings.
- Docs: this spec as built, the configuration guide (“Sharing settings with your team”), manual checks, including the three CVE scenarios as tests (a hook that would run before trust, a
base_urlin a project file, the same content at another path).
The review’s open questions were settled on their recommendations:
- Models in project files.
blitz.default_modeland[agent_models]are tier B, only for providers the user has set up, never over the user’s own choice;[model_settings]and every provider setting are never taken from a project. trust_workspace. Retired: deprecated, with the §8 meaning.- Tier A at open. No prompt; one line in the REPL (PRJ-32a), the full list in
/trust,doctorand the desktop’s Project settings. .blitz/stays writable by the agent. Any tier B change it makes asks for trust again.