@@ -23,12 +23,14 @@ const truncateMsg = "[ TRUNCATED -- set OPIK_CC_TRUNCATE_FIELDS=false ]"
2323
2424func 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
96145type tracingState struct {
0 commit comments