Skip to content

Commit f97ac47

Browse files
jverreclaude
andcommitted
feat: support [opik_cc] config section for plugin-scoped overrides
Make the ~/.opik.config parser section-aware so Claude Code workspace and project overrides can live in a dedicated [opik_cc] section instead of the awkward cc_workspace / cc_project_name keys under [opik]. SDK-shared values resolve from [opik] (with a top-level fallback for older flat configs). The deprecated cc_* keys are still honored for backward compatibility. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7bd0717 commit f97ac47

2 files changed

Lines changed: 82 additions & 25 deletions

File tree

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,26 @@ All plugin env vars use the `OPIK_CC_` prefix to avoid conflicts with standard O
6666
`OPIK_CC_WORKSPACE` and `OPIK_CC_PROJECT` let you send Claude Code traces to a different
6767
workspace/project than the rest of your Opik setup, without touching the global
6868
`OPIK_WORKSPACE` / `project_name` in `~/.opik.config` used by the Opik SDK. Both can also be
69-
set in `~/.opik.config` via the plugin-scoped `cc_workspace` and `cc_project` keys:
69+
set in `~/.opik.config` via a dedicated `[opik_cc]` section, which keeps the plugin's settings
70+
separate from the SDK's `[opik]` section:
7071

7172
```ini
7273
[opik]
7374
workspace = my-sdk-workspace # used by the Opik SDK
7475
project_name = my-sdk-project # used by the Opik SDK
75-
cc_workspace = my-cc-workspace # used only by the Claude Code plugin
76-
cc_project_name = my-cc-project # used only by the Claude Code plugin
76+
77+
[opik_cc]
78+
workspace = comet-all # used only by the Claude Code plugin
79+
project_name = claude-code # used only by the Claude Code plugin
7780
```
7881

79-
> Note: for backward compatibility, if `cc_project_name` is not set the plugin still falls back
80-
> to the shared `project_name` key.
82+
Resolution order (first match wins):
83+
84+
- **Workspace:** `OPIK_CC_WORKSPACE``[opik_cc] workspace``[opik] cc_workspace` (deprecated) → `[opik] workspace` / `OPIK_WORKSPACE`
85+
- **Project:** `OPIK_CC_PROJECT``[opik_cc] project_name``[opik] cc_project_name` (deprecated) → `[opik] project_name``claude-code` (default)
86+
87+
> Note: the deprecated `cc_workspace` / `cc_project_name` keys in the `[opik]` section are still
88+
> honored for backward compatibility, but new configs should use the `[opik_cc]` section.
8189
8290
### External Trace Linking
8391

src/config.go

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ const truncateMsg = "[ TRUNCATED -- set OPIK_CC_TRUNCATE_FIELDS=false ]"
2323

2424
func LoadConfig() (*Config, error) {
2525
homeDir, _ := os.UserHomeDir()
26-
var fileConfig map[string]string
26+
var sections sectionedConfig
2727
if homeDir != "" {
28-
fileConfig = parseConfigFile(filepath.Join(homeDir, ".opik.config"))
28+
sections = parseConfigFile(filepath.Join(homeDir, ".opik.config"))
2929
}
3030

31-
url := getEnvOrConfig("OPIK_BASE_URL", fileConfig, "url_override")
31+
// SDK-shared values live under the standard [opik] section (with a top-level
32+
// fallback for older flat config files).
33+
url := envOr("OPIK_BASE_URL", sections.opik("url_override"))
3234
if url == "" {
3335
return nil, nil
3436
}
@@ -38,38 +40,77 @@ func LoadConfig() (*Config, error) {
3840
cfg := &Config{
3941
URL: strings.TrimSuffix(url, "/") + "/v1/private",
4042
Project: "claude-code",
41-
APIKey: getEnvOrConfig("OPIK_API_KEY", fileConfig, "api_key"),
42-
Workspace: getEnvOrConfig("OPIK_WORKSPACE", fileConfig, "workspace"),
43+
APIKey: envOr("OPIK_API_KEY", sections.opik("api_key")),
44+
Workspace: envOr("OPIK_WORKSPACE", sections.opik("workspace")),
4345
Debug: os.Getenv("OPIK_CC_DEBUG") == "true" || tracing.debug,
4446
Truncate: os.Getenv("OPIK_CC_TRUNCATE_FIELDS") != "false",
4547
Enabled: tracing.enabled,
4648
ParentTraceID: os.Getenv("OPIK_CC_PARENT_TRACE_ID"),
4749
RootSpanID: os.Getenv("OPIK_CC_ROOT_SPAN_ID"),
4850
}
4951

50-
// OPIK_CC_PROJECT / cc_project are plugin-scoped and don't affect the Opik
51-
// SDK. project_name is kept as a fallback for backward compatibility, but it
52-
// is shared with the Opik SDK config in ~/.opik.config.
53-
if proj := getEnvOrConfig("OPIK_CC_PROJECT", fileConfig, "cc_project_name"); proj != "" {
52+
// Plugin-scoped project override. The [opik_cc] section keeps Claude Code
53+
// settings separate from the SDK's [opik] section. Precedence:
54+
// OPIK_CC_PROJECT > [opik_cc] project_name > [opik] cc_project_name (deprecated)
55+
// > [opik] project_name (shared SDK fallback for backward compatibility)
56+
if proj := os.Getenv("OPIK_CC_PROJECT"); proj != "" {
5457
cfg.Project = proj
55-
} else if proj := fileConfig["project_name"]; proj != "" {
58+
} else if proj := sections.cc("project_name"); proj != "" {
59+
cfg.Project = proj
60+
} else if proj := sections.opik("cc_project_name"); proj != "" {
61+
cfg.Project = proj
62+
} else if proj := sections.opik("project_name"); proj != "" {
5663
cfg.Project = proj
5764
}
5865

59-
// Allow overriding the workspace for the Claude Code plugin only, without
60-
// affecting the global OPIK_WORKSPACE / ~/.opik.config used by the Opik SDK.
61-
if ws := getEnvOrConfig("OPIK_CC_WORKSPACE", fileConfig, "cc_workspace"); ws != "" {
66+
// Plugin-scoped workspace override, so Claude Code traces can go to a
67+
// different workspace without touching the global OPIK_WORKSPACE / [opik]
68+
// workspace used by the Opik SDK. Precedence:
69+
// OPIK_CC_WORKSPACE > [opik_cc] workspace > [opik] cc_workspace (deprecated)
70+
if ws := os.Getenv("OPIK_CC_WORKSPACE"); ws != "" {
71+
cfg.Workspace = ws
72+
} else if ws := sections.cc("workspace"); ws != "" {
73+
cfg.Workspace = ws
74+
} else if ws := sections.opik("cc_workspace"); ws != "" {
6275
cfg.Workspace = ws
6376
}
6477

6578
return cfg, nil
6679
}
6780

68-
func parseConfigFile(path string) map[string]string {
69-
result := make(map[string]string)
81+
// sectionedConfig holds the parsed ~/.opik.config keyed by INI section name.
82+
// Keys that appear before any [section] header are stored under "".
83+
type sectionedConfig map[string]map[string]string
84+
85+
// value returns the key from the given section, or "" if absent.
86+
func (s sectionedConfig) value(section, key string) string {
87+
if vals := s[section]; vals != nil {
88+
return vals[key]
89+
}
90+
return ""
91+
}
92+
93+
// opik resolves an SDK-shared key from the [opik] section, falling back to a
94+
// top-level key for older flat config files.
95+
func (s sectionedConfig) opik(key string) string {
96+
if v := s.value("opik", key); v != "" {
97+
return v
98+
}
99+
return s.value("", key)
100+
}
101+
102+
// cc resolves a plugin-scoped key from the [opik_cc] section.
103+
func (s sectionedConfig) cc(key string) string {
104+
return s.value("opik_cc", key)
105+
}
106+
107+
func parseConfigFile(path string) sectionedConfig {
108+
sections := sectionedConfig{"": {}}
109+
current := sections[""]
110+
70111
file, err := os.Open(path)
71112
if err != nil {
72-
return result
113+
return sections
73114
}
74115
defer file.Close()
75116

@@ -79,18 +120,26 @@ func parseConfigFile(path string) map[string]string {
79120
if line == "" || strings.HasPrefix(line, "#") {
80121
continue
81122
}
123+
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
124+
name := strings.TrimSpace(line[1 : len(line)-1])
125+
if sections[name] == nil {
126+
sections[name] = map[string]string{}
127+
}
128+
current = sections[name]
129+
continue
130+
}
82131
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 {
83-
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
132+
current[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
84133
}
85134
}
86-
return result
135+
return sections
87136
}
88137

89-
func getEnvOrConfig(envVar string, fileConfig map[string]string, configKey string) string {
138+
func envOr(envVar, fallback string) string {
90139
if val := os.Getenv(envVar); val != "" {
91140
return val
92141
}
93-
return fileConfig[configKey]
142+
return fallback
94143
}
95144

96145
type tracingState struct {

0 commit comments

Comments
 (0)