Visitar URL original
[FEATURE]: Project and session configuration restrictions · Issue #63705 · anomalyco/opencode · GitHub
Skip to content

[FEATURE]: Project and session configuration restrictions #63705

Open

Description

@akaRem

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Related prior requests. #6361, #7163, and #15234, plus PR #16104 (closed, not merged), touch parts of this problem. We searched issues and PRs and did not find this exact combined design: per-project and per-session restricted configuration profiles with session-preserving transitions. Links are under Related.

Design review requested before implementation

This issue is a design review, not an implementation report. Nothing described here is implemented or tested yet. We ask maintainers to review the design before any implementation starts.

Summary

Add per-project and per-session configuration restrictions to opencode. A project runs in one of two persisted modes: Unrestricted, which loads configuration as today, and Restricted, which skips selected configuration sources. Each session records its own actual mode and profile, and restrictions can change mid-session without closing the session or losing the visible transcript. Everything ships behind an experimental flag.

Problem and motivation

opencode automatically loads configuration from the working directory: the project config file, instructions, agents, commands, skills, and plugins. Anyone with write access to a repository controls all of it. Opening a project you did not write means trusting all of it today.

  • A cloned repository can auto-execute MCP servers or plugins, and its instruction files can steer the model.
  • Users cannot inspect a project safely first, and they cannot run restricted review and unrestricted work as parallel sessions on the same server.
  • Teams sync configuration through git. A machine-specific or path-specific grant stored in that config would reach every clone.
  • Changing trust today means restarting with different settings. Users want to change it without forced session closes or lost visible history.
  • Scripts, CI, and Docker need a deterministic, non-interactive mode.

This feature is a trust boundary for configuration loading. It is not an absolute guarantee against remote code execution or prompt injection; see Limitations.

Prior art: VS Code Workspace Trust

VS Code ships Workspace Trust. The official guides are the reference for its behavior:

Aspect VS Code Workspace Trust This proposal
Trusted vs restricted result Trusting a folder re-enables tasks, debugging, terminal, extensions, and workspace settings. Restricted mode keeps them off. Unrestricted loads the six project sources as today. Restricted skips the sources the profile turns off. The usual tools keep running under the existing permission system; we add no sandbox.
Restricted banner and prompting With security.workspace.trust.startupPrompt: never, VS Code never asks; the window only shows the restricted banner. The one-time onboarding ask resolves the first choice, or the configured default choice (restrict or unrestrict) applies. There is no perpetual banner-only state.
Scope and inheritance Trust is keyed by workspace URI and inherited from parent folders. State is keyed by Project.ID and Session.ID on one server. Clones and worktrees with the same Project.ID on that server inherit; a different machine or database runs its own onboarding. Sessions on the same server stay independent.
What is restricted AI agents, tasks, debug, terminal, selected settings, and extensions. Configuration sources only — six whole-source keys. The usual tools keep their existing permissions.
Capability model Extensions declare restrictedConfigurations and get full, limited, or no capability. One on/off switch per source category. A source loads whole or not at all; there is no per-item capability negotiation.
Storage Per-user application storage on the client machine. The opencode server's existing SQLite database, as typed records. There is no universal cross-process stop: an unrestricted process elsewhere is not stopped by this design.
Applying a change Extension host restart or window reload. The session survives: profile-graph retirement, a tracked outstanding-work drain, and a hard provider-context reset under a new context_revision.

The differences follow from OpenCode's shape: long-lived sessions, MCP servers and plugins loaded from configuration, and a CLI and TUI that run headless. We do not claim undocumented parity with VS Code beyond the core trust idea, and we do not claim superiority.

Design

Two modes, one transient ask

There are exactly two persisted modes:

  • Unrestricted — profile ALL_ON. All six sources load, as today.
  • Restricted — any partial profile, or ALL_OFF. The sources the profile turns off do not load.

The onboarding ask is a transient state only. It is never stored and never becomes a third mode. State is keyed by the normalized profile, not by a path. Lock and "R" icons in this issue are candidate visual ideas only; showing the effective sources of a partially restricted profile needs further design exploration.

The six restricted sources

Restricted mode tracks six keys:

Key What it covers
opencode_json The whole project config: the direct file plus supplementary config.
instructions Project instruction files, boot-time and read-time AGENTS-style files.
agents Project agents, whole definitions: system prompt, model, request options, permissions.
commands Project commands, whole activation.
skills Project skills, whole activation.
plugins Project plugins, whole activation.
  • opencode_json accepts every declaration the config supports: providers, models, MCP servers, permissions, agents, plugins, remote instructions, and more. When this key is off, the file is not read, no preparse substitution runs, and nothing in it activates. There is no separate MCP or provider toggle inside the JSON; MCP servers declared there are covered by this key.
  • The .opencode, .claude, and .agents directories are classified by their actual consumers under these keys.
  • Built-in read, edit, and shell tools keep running under the existing permission system. There is no sandbox. Custom executable tools come from plugins, not from a .opencode/tools directory.
  • A project cannot enable this feature for itself through configuration.
  • The existing OPENCODE_DISABLE_PROJECT_CONFIG environment variable stays a stricter, separate switch above this feature.
  • Managed policy is unchanged.

Storage: typed state in the existing database

Restriction state lives in the opencode server's existing SQLite database as typed records. There is no trust JSON file: a JSON sidecar in the project would sit inside the directory an attacker controls, and it could not express typed server state.

  • The normalized profile record is called restricted_profile: a mode plus the on/off state of the six keys. Normalization means two profiles with the same meaning are the same record.
  • Project record: an editable restricted_profile plus a revision. It is the default for new standalone sessions.
  • Session record: the durable actual mode and profile, mode_revision, context_revision, and origin. Origin is one of inherited, project, session, or cli_override, and it changes only through an explicit transition.
  • Clones and worktrees with the same Project.ID on the same server inherit the project state. A different machine or database runs its own onboarding.
  • Plugins see the actual Session profile. They must not assume it equals the Project profile; that equality is false in general.
  • Sessions that predate this feature — and all archived sessions — are marked Unrestricted at revision 0. They are not closed. A session waiting on the onboarding ask pauses; the default-only path resumes it under its old Unrestricted state. Bulk operations never change archived sessions.

Onboarding: one-time, per project

Onboarding is one-time per project, not a perpetual fallback. A sibling experimental flag, restriction_onboarding, sets unconfigured_projects to one of ask (default), restrict, or unrestrict, with a prefill profile of ALL_OFF:

  • ask — a mandatory project dialog opens with an editable prefill. The saved choice becomes the Project's actual restrictions.
  • restrict — first onboarding persists the Project as Restricted with the partial set chosen.
  • unrestrict — first onboarding is an explicit host opt-in to ALL_ON.

Newly added projects follow the same policy. Later config changes apply only to not-yet-onboarded projects; existing projects and sessions are never updated or drained by a config change. After onboarding, a new session defaults to the Project's restrictions, not to global defaults. Nothing outside this onboarding treats ask as a persisted mode.

Upward config lookup is off by default

An experimental global/system/MDM/host setting, discover_project_config_upwards, defaults to false.

  • Lookup inside the selected location, through the Project root, is unchanged. Monorepos keep working.
  • With false, no configuration source above the Project root is read, in either mode.
  • With true, the legacy root search runs, but only for categories the profile enables.
  • A project cannot enable the setting for itself.
  • Designated global configuration is unaffected.

Changing this default is breaking, and we flag it for maintainer approval.

GUI and TUI

  • The current mode shows in the Composer next to the agent and model chips, in a hover/focus Popover, and in the mobile Drawer.
  • The TUI shows it in PromptMetadataRow, with a key-open detail dialog that adapts to the terminal size.
  • The detail view shows the Project default versus the actual Session profile, the origin, differences, a pending transition, an error state, and actions.
  • Settings → Projects gets a Project restrictions section. The Home manager fine-tunes mixed counts and bulk per-session outcomes.
  • The new-session welcome offers "This session only" or "Update project default", with explicit fine tuning for the Restricted→Unrestricted direction.
  • Active, archive, and inbox lists show restriction badges in both GUI and TUI. These are session-list badges, not per-source skipped badges.
  • Any exact icon — lock, "R", or otherwise — is deferred to visual exploration, as requested.

Changing restrictions mid-session

A Project-level change always offers exactly two choices:

  • New sessions only — current sessions keep running. Nothing changes for them and nothing drains.
  • Apply current — every unarchived session transitions, including descendants. Per the session search results, archived sessions are unchanged.

Applying current offers two timings:

  • Wait for a step boundary — the current LLM step finishes, nothing new starts, then tracked jobs cancel or drain.
  • Immediate — interrupt, awaitIdle, stop or wait for jobs and shells, plus a tracked registry drain.

A pending transition is reported with a typed status and a deadline. A session is never force-closed.

A mode change, or a change to any single source, writes a durable RestrictionsChanged event with the old and new normalized profiles and revisions. The Session ID and the full visible transcript survive, but all old provider context is hard reset in both directions: AGENTS content, synthetic messages, user and assistant messages, summaries and compaction, and native windows. The UI explicitly warns that model memory resets.

  • A fork gets its own durable context barrier, so pre-barrier context is never reinjected. The baseline is copied only when the fork has no other context.
  • Subagents inherit the parent profile.
  • A cross-project move applies the new profile and a new barrier.
  • Guards cover admissions: prompt, resume, title, generate, compaction, skill, fork, subagent, move, and the common LLM step boundary. Normal queued work continues after the change; a queued move is cancelled.

Safety mechanisms kept

  • Graphs are keyed by (Location.Ref, normalized profile content). The revision is excluded from the key, so identical profiles share graph content and different profiles stay isolated.
  • Gates run at source acquisition, before any read, fetch, import, or process.
  • Retirement applies only when the last borrower of a profile stops — never a generic reload.
  • A per-Session outstanding-work registry covers main, title, generate, compaction, Shell, subagent, and notification writers. They are cancelled or drained within a finite deadline.
  • Every writer carries sessionID plus context_revision. A stale writer is fenced before the RestrictionsChanged barrier. Drained, cancelled, or archived work never produces new model context.
  • awaitIdle alone is not sufficient.
  • Moving toward Restricted persists the restriction early and fails closed. After a cleanup failure or context_revision mismatch, an old Unrestricted state is never resumed.
  • Unrestricted startup is staged: it builds and drains first, then commits a marker. A failure leaves a working Restricted state.
  • Writes use an expected-revision CAS.
  • An execution interrupt is process-local. When a durable claim is held by another process, the result is ownership_conflict, not a fake global stop.
  • Local managed and standalone/private TUI deployments are supported. Remote Workspaces are not.
  • The default-only path does no cross-process drain.
  • There is no rollback of external code effects, no cleanup of detached processes, and no universal global stop.

Worktree startup

A stored worktree startup command may run when the Project is Unrestricted. Restricted or awaiting-choice projects block startup and record a pending state, with explicit retry after the Project becomes Unrestricted. A Session-only override never changes that Project-level baseline. Exactly-once startup after a crash is not promised.

CLI

  • Exact flag names: --unrestricted sets ALL_ON; --restricted sets ALL_OFF. Both are strict: they set the strict endpoint profiles, not the configured partial profile. Invalid combinations are parse errors.
  • The flags apply to this invocation and its new Session only. They are never written as the Project default. The actual Session record stores cli_override as origin, and that record is kept in the archive.
  • They do not touch --auto or headless deny semantics. Setting the opencode environment variables does not bypass managed policy.
  • Managed, standalone, and --server deployments must be capability-compatible; otherwise the server returns a typed mismatch error.
  • A headless run with no flag, while an ask is pending, exits with a descriptive non-zero status and remediation text. It does not wait on a modal dialog.
  • Resuming a session with an incompatible flag is an error that requires an explicit transition. It never silently resets.
  • ACP and mini clients are deferred.

Flag and delivery

The feature flag is project_restrictions, initially disabled in releases. Its value is resolved before project discovery, only from safe global/system/MDM/host sources, and is then fixed for the server process until restart. A project cannot enable the flag itself. The server reports its actual capability.

The experimental API lands first, then we run bun run generate in packages/client. Generated client files are never edited by hand.

Delivery is planned in three slices under the single flag:

  1. Dormant foundation — source gates, profile graphs, upward lookup, database migration, tests.
  2. Dormant runtime — session history, context fences, outstanding-work, transitions, startup, API tests.
  3. Complete client — GUI, TUI, CLI, and the flag enabled last.

Each slice is complete for its scope. The feature is not called complete until slice 3; a UI-only partial state is not a completion claim.

Planned test coverage: stale completions, generate and other outstanding model work, fork context barriers, source acquisition gates, above-root discovery defaults, prevention of self-authorization, no retroactive onboarding inheritance, revision CAS, crash recovery, external ownership conflicts, and local GUI/TUI integration. No tests exist yet.

Limitations

  • Enabling opencode_json intentionally accepts all its declarations, including MCP, providers, and plugins. General file content can still steer the model. Partial-profile choices remain the user's responsibility. This is not a sandbox.
  • Previously caused side effects and detached processes cannot be undone. A transition cannot undo what already ran.
  • Remote Workspaces are outside the initial scope. There is also no universal immediate-stop guarantee across independent processes.
  • Exactly-once behavior across a crash is not guaranteed.

Questions for maintainers

  1. Naming — are the final names right: project_restrictions, restriction_onboarding, discover_project_config_upwards, restricted_profile, and the six source keys?
  2. Scope — are six whole-source keys the right scope, or should more or fewer sources be restricted?
  3. Onboarding — are the ask default, the prefill profile, and the one-time-per-project policy right?
  4. Model reset — is the hard provider-context reset on every transition acceptable, given the explicit warning?
  5. Upward lookup — do maintainers approve the breaking discover_project_config_upwards default of false?
  6. Process model — is ownership_conflict the right result for a cross-process claim conflict?
  7. Tracking — what should the pending-transition deadline and typed reporting look like?

JSON-declared MCP servers are covered by opencode_json; ownership_conflict specifically concerns execution claims owned by another process.

Related

Activity

  1. opencodejr commented on Nov 13, 2026

    @opencodejr

    Thanks for the detailed design, @akaRem. As I understand it, you're proposing per-project and per-session restricted configuration profiles: opening a repository you didn't write would no longer automatically load its opencode.json, instructions, agents, commands, skills or plugins. There would be a one-time choice for each project, and you could switch modes mid-session without losing the visible transcript. All of it would sit behind an experimental flag.

    Maintainers will look at the design. I can't say whether or when it would be accepted. They will probably weigh the breaking part (turning off upward config lookup by default) separately from the rest.

    Related issues, for context:

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

    area/coreAgent loop, tools and configtype/featureNew behaviour or an improvementversion/v2OpenCode 2.x

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions