[#185] Warn on corrupt/unreadable files in ConfigurationDiscovery#193
[#185] Warn on corrupt/unreadable files in ConfigurationDiscovery#193
Conversation
…nDiscovery - Split guard...try?...else patterns into fileExists (silent return) and do/catch (output.warn) across 9 locations - Preserves best-effort discovery design while giving users diagnostic feedback for corrupt/unreadable files - Keep try? only for symlink detection in filter closure (benign fallback)
There was a problem hiding this comment.
Pull request overview
This PR improves ConfigurationDiscovery diagnostics during mcs export by warning when configuration files/directories exist but are unreadable or corrupt, while keeping missing files silent and preserving best-effort behavior.
Changes:
- Replaced several
try?-based silent failures withfileExists+do/catchandoutput.warn(...)on read/parse failures. - Added warnings for partial failures when deserializing/serializing remaining settings JSON.
- Kept discovery APIs non-throwing so export continues after warnings.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { | ||
| output.warn("Could not parse \(claudeJSONPath.lastPathComponent) as JSON — file may be corrupt") | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
JSONSerialization.jsonObject(with:) can throw, but this uses try? so the warning loses the underlying parse error details. To match the goal of warning with actionable error context, switch this to a do/catch and include error.localizedDescription (and optionally distinguish 'not a dictionary' from 'invalid JSON').
| guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { | |
| output.warn("Could not parse \(claudeJSONPath.lastPathComponent) as JSON — file may be corrupt") | |
| return | |
| } | |
| let jsonAny: Any | |
| do { | |
| jsonAny = try JSONSerialization.jsonObject(with: data) | |
| } catch { | |
| output.warn("Could not parse \(claudeJSONPath.lastPathComponent) as JSON: \(error.localizedDescription)") | |
| return | |
| } | |
| guard let json = jsonAny as? [String: Any] else { | |
| output.warn("Expected top-level JSON object in \(claudeJSONPath.lastPathComponent) — file may be misconfigured") | |
| return | |
| } |
| do { | ||
| settings = try Settings.load(from: settingsPath) | ||
| } catch { | ||
| output.warn("Could not parse \(settingsPath.lastPathComponent): \(error.localizedDescription)") |
There was a problem hiding this comment.
These catch blocks warn "Could not parse ..." but Settings.load(from:) can also fail due to I/O (e.g. permission denied) rather than JSON parsing. Consider changing the wording to "Could not load" / "Could not read or parse" so the warning remains accurate across failure modes.
| output.warn("Could not parse \(settingsPath.lastPathComponent): \(error.localizedDescription)") | |
| output.warn("Could not read or parse \(settingsPath.lastPathComponent): \(error.localizedDescription)") |
| settings = try Settings.load(from: settingsPath) | ||
| } catch { | ||
| output.warn("Could not parse \(settingsPath.lastPathComponent): \(error.localizedDescription)") | ||
| return [:] |
There was a problem hiding this comment.
Same as above: this warns "Could not parse ..." but the thrown error may be an I/O failure (unreadable file) rather than a JSON parse failure. Adjusting the message to "Could not load" / "Could not read or parse" would avoid misleading diagnostics.
… try? - Fold hook-command extraction into discoverSettings (returns [String: String]?) to eliminate duplicate Settings.load call - Convert last try? on JSONSerialization in discoverMCPServers to do/catch for consistent error surfacing
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| do { | ||
| settings = try Settings.load(from: settingsPath) | ||
| } catch { | ||
| output.warn("Could not parse \(settingsPath.lastPathComponent): \(error.localizedDescription)") |
There was a problem hiding this comment.
The warning message in this catch labels all failures as "Could not parse", but Settings.load(from:) can also throw for I/O errors (e.g., permission denied / unreadable file). Consider changing this to a more accurate message (e.g., "Could not load …") or branching on the error so read failures and JSON decode failures are described correctly.
| output.warn("Could not parse \(settingsPath.lastPathComponent): \(error.localizedDescription)") | |
| output.warn("Could not load \(settingsPath.lastPathComponent): \(error.localizedDescription)") |
| private func discoverSettings(at settingsPath: URL, hooksDir: URL, into config: inout DiscoveredConfiguration) -> [String: String]? { | ||
| let settings: Settings |
There was a problem hiding this comment.
hooksDir is passed into discoverSettings but is not used anywhere in the implementation. If it’s no longer needed, remove the parameter (and update the call site) to reduce API noise; if it is needed, please use it (e.g., for more specific warnings).
| do { | ||
| files = try fm.contentsOfDirectory(at: hooksDir, includingPropertiesForKeys: nil) | ||
| } catch { | ||
| output.warn("Could not read hooks directory: \(error.localizedDescription)") |
There was a problem hiding this comment.
This warning doesn’t identify which hooks directory failed to read (global vs project, and the path/name), which can make it hard to diagnose in multi-scope runs. Consider including hooksDir.path or hooksDir.lastPathComponent in the message (similar to listFiles).
| output.warn("Could not read hooks directory: \(error.localizedDescription)") | |
| output.warn("Could not read hooks directory at \(hooksDir.path): \(error.localizedDescription)") |
- Use "Could not load" instead of "Could not parse" for Settings.load failures (covers both I/O and decode errors) - Include hooks directory path in warning for easier diagnostics
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let settings: Settings | ||
| do { | ||
| settings = try Settings.load(from: settingsPath) | ||
| } catch { | ||
| output.warn("Could not load \(settingsPath.lastPathComponent): \(error.localizedDescription)") | ||
| return nil |
There was a problem hiding this comment.
The warning in the catch uses “Could not parse …” but Settings.load(from:) can also throw for non-parse failures (e.g., read/permission errors from Data(contentsOf:)). Consider wording this as “Could not load …” or “Could not read/parse …” so the message matches the actual failure mode.
Context
ConfigurationDiscoveryusedguard...try?...else { return }for every discovery method, collapsing distinct failure scenarios (file missing vs. corrupt vs. permission denied) into the same silent empty result. Users runningmcs exportgot incomplete exports with no indication of why artifacts were missing.Changes
try?patterns intofileExistscheck (silent early return for missing files) followed bydo/catchwithoutput.warn(...)for corrupt/unreadable filesdiscoverMCPServers,discoverSettings,extractHookCommands,discoverFiles,listFiles,discoverClaudeContent,discoverGitignoreEntriestry?only for symlinkresourceValuesdetection inside a.filterclosure (benign fallback)Acceptance Criteria
[WARN]message with error detailsTesting Steps
swift build— compiles cleanlyswift test— all 585 tests passmcs export /tmp/test-exporton a working project — same output, no warnings~/.claude.json(e.g.echo "{{bad" > ~/.claude.json) →[WARN] Could not parse .claude.json as JSON — file may be corrupt, export continues without MCP serversCloses #185