You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Introduce 'model-cli config' as a new top-level command with an interface
and file format inspired by, but not referencing, 'git config'.
New cmd/cli/iniconfig package: parses and writes INI-style config files
(section headers, subsections, boolean keys, inline comments, backslash
escapes, quoted values, UTF-8 BOM). Writes are atomic via .lock + rename.
New 'config' command with subcommands: get, set, unset, list, edit.
All subcommands accept --global (default per XDG_CONFIG_HOME or
~/.config/model-runner/config), --system (/etc/model-runner/config),
and --file/-f flags.
Remove the 'config' alias from 'configure' to avoid a name collision;
'configure' remains hidden and undocumented for existing callers.
'config' requires no running model-runner instance (pure local file I/O)
and is registered outside the withStandaloneRunner group.
Parser: handle trailing comments on section headers ([core] # comment),
raise a clear error on lines exceeding 1 MiB, preserve existing file
permissions on write (default 0600 for new files).
Editor: split VISUAL/EDITOR on whitespace to support values like
'code --wait'.
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 4 issues, and left some high level feedback:
In unescapeSubsection the comment says unknown backslash escapes are an error, but the implementation silently drops the backslash (matching git); please update the comment to accurately describe the behavior or change the code to match the documented contract.
In parseValue the comment mentions line continuation via trailing backslash, but the function currently just treats a trailing backslash as end-of-value with no continuation; consider either implementing proper multi-line continuation or updating the comment to avoid implying continuation is supported.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `unescapeSubsection` the comment says unknown backslash escapes are an error, but the implementation silently drops the backslash (matching git); please update the comment to accurately describe the behavior or change the code to match the documented contract.
- In `parseValue` the comment mentions line continuation via trailing backslash, but the function currently just treats a trailing backslash as end-of-value with no continuation; consider either implementing proper multi-line continuation or updating the comment to avoid implying continuation is supported.
## Individual Comments### Comment 1
<locationpath="cmd/cli/gitconfig/gitconfig.go"line_range="75-84" />
<code_context>
+ var entries []Entry
+ var section, subsection string
++ scanner := bufio.NewScanner(bytes.NewReader(data))
+ lineNum := 0
+ for scanner.Scan() {
+lineNum+++line := scanner.Text()+trimmed := strings.TrimSpace(line)++// Empty line or comment.+if trimmed == "" || trimmed[0] == '#' || trimmed[0] == ';' {+ continue+}++if trimmed[0] == '[' {+ // Section header.+ var err error+ section, subsection, err = parseSectionHeader(trimmed)+ if err != nil {+ return nil, fmt.Errorf("line %d: %w", lineNum, err)+ }+ continue+}++// Key-value (or boolean key).+if section == "" {+ return nil, fmt.Errorf("line %d: key outside of section", lineNum)+}+key, value, err := parseKeyValue(line)+if err != nil {+ return nil, fmt.Errorf("line %d: %w", lineNum, err)+}+canonical := canonicalKey(section, subsection, key)+entries = append(entries, Entry{Key: canonical, Value: value})+ }
+ return entries, scanner.Err()
+}
+
</code_context>
<issue_to_address>
**suggestion:** Consider guarding against very long lines when using bufio.Scanner
`bufio.Scanner` limits token size to 64KB; lines longer than that will cause `scanner.Err()` to be `ErrTooLong`. If long config values are possible (e.g., certificates, large tokens), consider increasing the buffer with `scanner.Buffer(..., maxCapacity)` and handling `ErrTooLong` explicitly rather than treating it as a generic parse error.
Suggested implementation:
```golangimport (
"bufio""bytes""errors""fmt""io""os""path/filepath""strings""unicode"
)
const maxConfigLineBytes = 1 << 20// 1 MiB max line length for gitconfig lines``````golangvarentries []Entryvarsection, subsectionstringscanner:= bufio.NewScanner(bytes.NewReader(data))
// Increase the scanner buffer to allow reasonably large config lines// (e.g. certificates, large tokens). The initial size is small, but// maxConfigLineBytes is the hard cap.
scanner.Buffer(make([]byte, 0, 64*1024), maxConfigLineBytes)
lineNum:=0for scanner.Scan() {
lineNum++
line:= scanner.Text()
trimmed:= strings.TrimSpace(line)
// Empty line or comment.if trimmed == "" || trimmed[0] == '#' || trimmed[0] == ';' {
continue
}
if trimmed[0] == '[' {
// Section header.varerrerror
section, subsection, err = parseSectionHeader(trimmed)
if err != nil {
returnnil, fmt.Errorf("line %d: %w", lineNum, err)
}
continue
}
// Key-value (or boolean key).if section == "" {
returnnil, fmt.Errorf("line %d: key outside of section", lineNum)
}
key, value, err:=parseKeyValue(line)
if err != nil {
returnnil, fmt.Errorf("line %d: %w", lineNum, err)
}
canonical:=canonicalKey(section, subsection, key)
entries = append(entries, Entry{Key: canonical, Value: value})
}
iferr:= scanner.Err(); err != nil {
if errors.Is(err, bufio.ErrTooLong) {
// The failing line is the next line after the last one we// successfully scanned.returnnil, fmt.Errorf("line %d: config line too long (max %d bytes)", lineNum+1, maxConfigLineBytes)
}
returnnil, err
}
return entries, nil```
</issue_to_address>
### Comment 2
<locationpath="cmd/cli/gitconfig/gitconfig.go"line_range="206-208" />
<code_context>
+case inQuotes && c == '"':+ inQuotes = false+ i+++case c == '\\':+ if i+1 >= len(raw) {+ // Trailing backslash = line continuation (we don't handle+ // multi-line here; treat as end of value).+ goto done
</code_context>
<issue_to_address>
**issue (bug_risk):** Line-continuation handling with trailing backslash may drop content silently
This logic treats a trailing backslash as "end of value" and discards both the backslash and any intended continuation, which diverges from git’s continuation semantics and can silently truncate values if a user adds a trailing `\`. Consider either implementing continuation by reading the next physical line(s), or treating an unterminated escape at end-of-line as a parse error to avoid silent data loss.
</issue_to_address>
### Comment 3
<locationpath="cmd/cli/gitconfig/gitconfig.go"line_range="405" />
<code_context>
+ newEntries := transform(append([]Entry(nil), f.entries...))
++ data := serialise(newEntries)
+ if err := os.WriteFile(lockPath, data, 0o644); err != nil {
+return err+ }
</code_context>
<issue_to_address>
**🚨 issue (security):** Writing with fixed 0644 permissions may weaken existing file permissions
Hardcoding 0644 means a previously more restrictive config (e.g., 0600 with secrets) will be relaxed on rewrite. Consider preserving the existing file mode when the file already exists, or defaulting to a more restrictive mode (e.g., 0600) for per-user configs. You can `os.Stat(f.path)` to reuse its mode and fall back to a secure default otherwise.
</issue_to_address>
### Comment 4
<locationpath="cmd/cli/commands/config.go"line_range="310-314" />
<code_context>
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {+ return err+ }+ if _, err := os.Stat(path); os.IsNotExist(err) {+ f, err2 := os.Create(path)+ if err2 != nil {+ return err2+ }+ _ = f.Close()+ }+
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Newly created config file in `config edit` likely should use restrictive permissions
`os.Create` will respect the umask but will usually produce a file around 0644, which is too permissive if this config can store tokens or other sensitive data. Please create the file with an explicit 0600 mode (e.g., via `os.OpenFile` with the desired permissions) so it behaves like a private user config file.
```suggestion // Ensure the file (and its parent directory) exist so the editor // has something to open. if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } if _, err := os.Stat(path); os.IsNotExist(err) { f, err2 := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o600) if err2 != nil { return err2 } _ = f.Close() }```
</issue_to_address>
Sourcery is free for open source - if you like our reviews please consider sharing them ✨
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a new config command to the CLI, enabling users to read and write configuration values in a git-style INI format. The implementation includes a new gitconfig package for parsing and serializing these files, along with comprehensive tests. Key feedback includes a critical security concern regarding the preservation of file permissions during atomic writes, a parsing limitation where trailing comments on section headers cause failures, and an issue with the edit command failing when environment variables like EDITOR contain arguments.
The reason will be displayed to describe this comment to others. Learn more.
The writeAtomic function uses a hardcoded file mode of 0o644 when writing the config file. This is a security risk as it can make sensitive configuration files (e.g., those containing API keys or tokens) world-readable, even if the original file had more restrictive permissions (like 0600). The implementation should preserve the permissions of the existing file if it exists.
The reason will be displayed to describe this comment to others. Learn more.
The edit command uses exec.CommandContext with the editor string directly as the executable name. This will fail if the VISUAL or EDITOR environment variables contain arguments (e.g., EDITOR="code --wait" or EDITOR="vim -R"). To support common user configurations, the command string should be parsed into an executable and its arguments before being passed to exec.CommandContext.
ericcurtin
changed the title
add model-cli config command with git-format INI file support
add model-cli config command with INI file support
Apr 28, 2026
Introduce 'model-cli config' as a new top-level command with an interface
and file format inspired by, but not referencing, 'git config'.
- New cmd/cli/iniconfig package: parses and writes INI-style config files
(section headers, subsections, boolean keys, inline comments, backslash
escapes, quoted values, UTF-8 BOM). Writes are atomic via .lock + rename.
- New 'config' command with subcommands: get, set, unset, list, edit.
All subcommands accept --global (default per XDG_CONFIG_HOME or
~/.config/model-runner/config), --system (/etc/model-runner/config),
and --file/-f flags.
- Remove the 'config' alias from 'configure' to avoid a name collision;
'configure' remains hidden and undocumented for existing callers.
- 'config' requires no running model-runner instance (pure local file I/O)
and is registered outside the withStandaloneRunner group.
- Parser: handle trailing comments on section headers ([core] # comment),
raise a clear error on lines exceeding 1 MiB, preserve existing file
permissions on write (default 0600 for new files).
- Editor: split VISUAL/EDITOR on whitespace to support values like
'code --wait'.
- Regenerate CLI reference docs.
The reason will be displayed to describe this comment to others. Learn more.
can do something like
count := 0
for _, configFlag := range []bool{global, system, file != ""} {
if configFlag{
count++
}
}
if count > 1 {
return "", fmt.Errorf("only one of --global, --system, or --file may be specified")
}
Pushed 72fb485: flag-count and get cleanups, plus fixes for empty VISUAL/EDITOR, file mode under umask, trailing backslash, ] in quoted subsections, and control chars in subsections. Tests added.
Malformed keys are treated as missing because Get/GetAll swallow ParseKey errors. Consequently, config get --default fallback core..name returns fallback instead of rejecting the invalid key, unlike set and unset. Validate the key with iniconfig.ParseKey before querying so --default only applies to a valid but absent key.
Validate subsection control characters during parsing
cmd/cli/iniconfig/iniconfig.go:163
The parser does not apply the same control-character validation to subsections that ParseKey applies later. A file containing a subsection with \r or \x00 is accepted into Entries, but Get/Set reject the corresponding key, making that entry inaccessible and preventing a later write from addressing it. Reject \n, \r, and NUL after unescaping here, matching ParseKey.
Validate the first Unicode rune instead of its UTF-8 byte
cmd/cli/iniconfig/iniconfig.go:349
name[0] is the first UTF-8 byte, not the first rune. Because the rest of this validator uses unicode.IsLetter, valid letter-starting names such as Hebrew א can be rejected (its leading byte is interpreted as ×), while a non-letter Unicode digit can pass the initial check. Decode the first rune before applying the letter check, or consistently restrict the whole name to ASCII as the comment claims.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduce 'model-cli config' as a new top-level command with an interface
and file format inspired by, but not referencing, 'git config'.
(section headers, subsections, boolean keys, inline comments, backslash
escapes, quoted values, UTF-8 BOM). Writes are atomic via .lock + rename.
All subcommands accept --global (default per XDG_CONFIG_HOME or
~/.config/model-runner/config), --system (/etc/model-runner/config),
and --file/-f flags.
'configure' remains hidden and undocumented for existing callers.
and is registered outside the withStandaloneRunner group.
raise a clear error on lines exceeding 1 MiB, preserve existing file
permissions on write (default 0600 for new files).
'code --wait'.