feat(setup): report an unwritable target as a structured error (task 0044)
All checks were successful
CI / test (22) (pull_request) Successful in 50s
CI / test (true, 24) (pull_request) Successful in 1m5s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 47s
CI / test (true, 24) (push) Successful in 1m4s
CI / flake (push) Successful in 3s

Both halves of `setup` assumed the files they manage are writable. A
declaratively managed target — read-only because a configuration manager
owns it, because a file is flagged immutable, or because the path is
root-owned — made the skill install raise a raw filesystem exception and
the hook install surface the underlying message with no guidance.

Both now fail with `TARGET_NOT_WRITABLE`, naming the file and pointing at
the general remedy: it appears to be managed by another tool, so declare
the skill or hook through that configuration instead. The error names no
particular manager, because read-only is not diagnostic of one.

A target already byte-identical to the bundled copy still succeeds —
nothing needs writing, so its being read-only is beside the point.
This commit was merged in pull request #53.
This commit is contained in:
2026-07-20 13:25:03 -04:00
parent 27aad04984
commit a1e68dc530
5 changed files with 283 additions and 23 deletions

View File

@@ -21,9 +21,32 @@ The failure follows the CLI's existing error convention rather than inventing a
## Acceptance criteria
- [ ] An unwritable skill target produces a structured CLI error rather than an unhandled filesystem exception.
- [ ] An unwritable hook target produces the same class of error, with the same guidance.
- [ ] Both errors name the file that could not be written and state that it appears to be managed by another tool.
- [ ] Neither error names or infers a specific configuration manager.
- [ ] A skill target that is unwritable but already byte-identical to the bundled copy succeeds rather than failing, since nothing needs to be written.
- [ ] The errors carry a code and help lines consistent with the rest of the CLI's error surface.
- [x] An unwritable skill target produces a structured CLI error rather than an unhandled filesystem exception.
- [x] An unwritable hook target produces the same class of error, with the same guidance.
- [x] Both errors name the file that could not be written and state that it appears to be managed by another tool.
- [x] Neither error names or infers a specific configuration manager.
- [x] A skill target that is unwritable but already byte-identical to the bundled copy succeeds rather than failing, since nothing needs to be written.
- [x] The errors carry a code and help lines consistent with the rest of the CLI's error surface.
## Implementation Notes
The condition is `EACCES`, `EPERM`, or `EROFS` — the three ways a filesystem refuses a write for a reason the user has to settle outside this tool.
The new `TARGET_NOT_WRITABLE` code joins the spec's enumerated list, alongside a paragraph describing it.
Two things came out of review and go slightly beyond the literal criteria.
The skill half now guards its comparison read as well as its write.
A target the filesystem will not let us read is one it will not let us replace either — the same condition reached one call earlier — so a mode-`000` file reports the same error rather than the raw exception the criteria were written against.
The hook half collects its failures as `{path, detail}` records rather than the agent SDK's flattened `<path>: <message>` text.
The SDK reports through a string, so the two halves are separated once at that boundary and judged apart.
This matters for correctness, not just shape: testing the whole formatted string for an errno would misclassify an unrelated failure whose *path* happened to contain `EACCES`.
Two known limits, both judged acceptable rather than fixed.
The hook error names the target the SDK was writing, which is the intended path rather than necessarily the blocking one — if `~/.claude` were unwritable and `settings.json` absent, it would name the file rather than the directory.
The SDK discards the error object, so its `path` is not recoverable; the skill half, which catches its own errors, does report the blocking path and is tested for it.
An unwritable `~/.claude/settings.json` still leaves the Codex and OpenCode integrations installed, because the SDK writes them before the failure surfaces.
The command exits 1 having done part of its work.
Making the hook install transactional across three integrations owned by the SDK is a larger change than this task, and re-running after fixing the permission converges correctly.