Skip to content

fix(db): distinguish permission-denied discovery - #278

Open
matheuscoelhomalta wants to merge 2 commits into
ryanlewis:mainfrom
matheuscoelhomalta:fix/macos-db-permission-diagnostics
Open

matheuscoelhomalta wants to merge 2 commits into
ryanlewis:mainfrom
matheuscoelhomalta:fix/macos-db-permission-diagnostics

Conversation

@matheuscoelhomalta

@matheuscoelhomalta matheuscoelhomalta commented Sep 18, 2026 •

Copy link
Copy Markdown

Summary

  • replace filepath.Glob database discovery with permission-aware directory traversal
  • preserve EPERM/EACCES during discovery and database opening instead of reporting the database as missing or reducing the error to SQLite code 14
  • probe one byte read-only before SQLite opens the file so callers can identify the underlying macOS permission denial
  • add things doctor with plain and JSON diagnostics that do not query task data
  • document the diagnostic command in the README, site docs, install guide, and bundled agent skill

Why

filepath.Glob returns an empty match set when a process cannot enumerate Things' app-group container. The CLI therefore reports Things3 database not found even when the database exists and the actual failure is a macOS privacy denial.

This was observed on macOS 27 when things was launched by a desktop agent process. The same database and v0.8.0 binary remained readable over SSH, so this change does not attempt to bypass macOS privacy controls; it makes the failure accurate and inspectable.

things doctor reports automatic/config/flag selection, the inspected paths, a stable status such as permission_denied, and whether the selected database opens read-only. A completed diagnosis exits successfully even when ok is false so JSON consumers always receive the report.

The final commit was exercised in the affected macOS 27 desktop-agent context. Automatic discovery changed from the misleading database not found result to permission_denied; an explicit --db path also reports permission_denied and preserves operation not permitted instead of reducing the cause to SQLITE_CANTOPEN (14). Normal task listing remains blocked by macOS in that context, as expected: this change diagnoses the privacy denial and does not bypass it.

Verification

  • go test -race ./...
  • golangci-lint v2.12.2 run ./...
  • go build ./cmd/things
  • things --json doctor against a live Things database
  • things --json list today against the same database to confirm normal discovery remains functional
  • macOS 27 affected-context test of automatic discovery, normal listing, and explicit --db diagnosis at commit ecc2b98

The permission-denied paths are covered with injected readdir, stat, and open failures, so the tests do not depend on changing the runner's real macOS privacy settings.

@ryanlewis

Copy link
Copy Markdown
Owner

Thanks for the PR @matheuscoelhomalta ! Please give me a day or two to get round to taking a look at this.

@ryanlewis ryanlewis left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @matheuscoelhomalta. It's a really useful fix. Getting "database not found" when macOS is actually refusing access is confusing, and the injected-failure tests are a nice way to cover it without touching TCC settings.

A few things before merging:

  1. A stray ThingsData-* file now stops discovery. If a file (not a folder) matching the prefix is in the container, stat returns ENOTDIR. That falls through to the default case, so discovery fails. filepath.Glob used to skip these. Could you treat syscall.ENOTDIR like not-exist and add a test for it?
  2. README placement: the new ### Database diagnostics section sits inside the Configuration section. The paragraph after it ("Anything set here still loses to a flag...") now reads as if it's about doctor. Could you move it to just before ### Listing tasks?
  3. Exit code: every other command exits non-zero on failure. I'd like doctor to do the same: still print the full report to stdout, but exit 1 when ok is false. Then things doctor && ... works as you'd expect. Happy to talk about it if you feel strongly the other way.
  4. Optional: if one ThingsData-* folder is unreadable but another has the database, discovery now fails where it used to succeed. You could remember the permission error and only return it when nothing matched.

Everything else looks good to me. Thanks again!

@ryanlewis

Copy link
Copy Markdown
Owner

Hi @matheuscoelhomalta, just checking in on this. main has moved on quite a bit since, so it'll need a rebase as well as the changes from my review. No rush, and if you'd rather I take it from here, just say so.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants