Skip to content

opencode will use default values instead of user config values when a project level config exists #16897

@amelvil2-ford

Description

@amelvil2-ford

Description

Quick summary

It seems like the config hierarchy has some problems, specifically that the defaults coded into typescript can override the user configs when a project settings file is present.

From what I was able to find, this does not affect any config setting that opencode currently has.

  • It seems like no setting (other than keybinds) in opencode has a default that isn't just an empty string or whatever the default value of a type is (maybe that was a deliberate workaround for this?)
  • I was not able to reproduce this with a keybind

So that means that the config system has a limitation, but as far as I know, it's not something a user would notice.

About my use case:

  • This limitation is unfortunate because there's a setting I'd like to add that would have a default value that's not an empty string
  • I would like to allow users to set it to an empty string, but I don't want that to be the default

What I've noticed from debugging

In state(), the way user settings are retrieved via global() seems to have special behavior

result = mergeConfigConcatArrays(result, await global())

If I trace this problem,

  • It seems like the default values are not present on what's returned from global()
  • Config files loaded after the initial user config (e.g. project level config files) will load default values
  • That means that if a config field with default values is not present in a project level config, but is present in a user config, it will take the default value rather than the user customized setting

Unfortunately a fix for this behavior might be considered a breaking change.

Possible workarounds:

A. Set a default value, and just have users put the field in their project level config as well as their user config (not ideal).

B. Don't use default("The default string value I want") and interpret whatever zod gives me for an unset field as a signal to use the default string

  • Fields without defaults will have a value of undefined
  • I can treat a value of undefined as a signal to use the default value, though this is not super ideal from a coding perspective. A typo would look the same as intended behavior.

Plugins

None

OpenCode version

v1.2.24

Steps to reproduce

1. Add a new config field with a default

At the moment the way to reproduce this is to add a new config field,

For example, add a new field with a default value to Info:

export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
logLevel: Log.Level.optional().describe("Log level"),
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
command: z

Like this:

  export const Info = z
    .object({
      $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
      testField: z
        .string()
        .optional()
        .describe("A field for testing config hierarchies")
        .default("defaultValue"),

2. Add some code that uses this setting

Feel free to use any method that you want to access this setting, there's probably an easier way to do this.

For the sake of this test, I hacked it into this function:

async function session(sdk: OpencodeClient) {
const baseID = args.continue ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id : args.session
if (baseID && args.fork) {
const forked = await sdk.session.fork({ sessionID: baseID })
return forked.data?.id
}
if (baseID) return baseID
const name = title()
const result = await sdk.session.create({ title: name, permission: rules })
return result.data?.id
}

I edited this function to look like this:

async function session(sdk: OpencodeClient) {
  const baseID = args.continue ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id : args.session

  if (baseID && args.fork) {
    const forked = await sdk.session.fork({ sessionID: baseID })
    return forked.data?.id
  }

  if (baseID) return baseID

  //const name = title()
  // HACK: to test custom config value
  let configStr;

  const config = await sdk.config.get()

  if(config) {
    configStr = "config.data is undefined"
    if(config.data) {
      configStr = "config.data.testField is undefined"
      if(config.data.testField) {
        configStr = config.data.testField;
      }
    }
  }

  const name = `The value of testField is ${configStr}`
  const result = await sdk.session.create({ title: name, permission: rules })
  return result.data?.id
}

Usage

With the edits in this step, whenever you do this

opencode run "what is 2 + 2"

It will put The value of testField is ... in the name of the session created by opencode run. Note that this will NOT work from the TUI.

3. Demo: The default value is working

  1. Build opencode by cd'ing into packages/opencode and doing bun run build
  2. create an empty directory somewhere on your computer
  3. Run /path/to/your/built/opencode run "what is 2 + 2"
  4. Run opencode session list

You should see a new session named "The value of testField is defaultValue"

4. Demo: Users can override this default value in their user config for directories that do not contain a .opencode directory

  1. Edit your user config to set this value

E.g. in MacOS, put this in ~/.config/opencode/opencode.jsonc

{
  "$schema": "https://opencode.ai/config.json",
  "testField": "userConfig"
}
  1. In the empty directory, run /path/to/your/built/opencode run "what is 2 + 2" again
  2. Run opencode session list

You should see a new session named "The value of testField is userConfig"

5. The bug: If you run opencode in a directory containing .opencode/opencode.jsonc, it will apply default values rather than the user configuration

  1. In the empty directory, create a directory named .opencode and put this opencode.jsonc in it
{
  "$schema": "https://opencode.ai/config.json"
}

Note that it does NOT change the value of testField.

  1. In the directory containing the .opencode directory, run /path/to/your/built/opencode run "what is 2 + 2" again
  2. Run opencode session list

You should see a new session named "The value of testField is defaultValue"

  • I would have expected it to say "userConfig".

Note that you can override this setting at the project level too, and you can "fix" this by copying your config from your ~/.config/opencode directory into the project, but you'd have to do that for each directory you're in.

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions