Skip to content

Tool-call inspection guards ThinkWatch's own data directory; the docs say what protects config.yaml - #287

Merged
fylorn merged 1 commit into
mainfrom
fix/guard-own-data-dir
Oct 4, 2026
Merged

fylorn merged 1 commit into
mainfrom
fix/guard-own-data-dir

Conversation

@fylorn

@fylorn fylorn commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Refs #286.

What

  • New built-in tool rule thinkwatch-data (Read or change ThinkWatch's own data; high, so cut under enforce). It fires when a tool call's path or command points into ThinkWatch's data directory: ~/.thinkwatch (any .thinkwatch path component), %APPDATA%\ThinkWatch, and /var/lib/thinkwatch or /etc/thinkwatch on a server. Case-insensitive, since macOS file systems are by default.
  • Docs: the configuration reference ("Where the file is", en and zh-CN) and the README now say what protects config.yaml: 0600/0700 keeps out other users, not programs running as the same user; outbound redaction protects what leaves the machine, not the file; the control key is masked so that a control-plane write cannot change it, not to hide it from local programs. The generated rule table lists the new rule.

Why a code check

Mentioning the path is not touching it: edits to docs, comments and answers about where the config lives are full of ~/.thinkwatch, and a regex that fires on any occurrence would cut those sessions off. So the check reads arguments by role (crates/tw-guard/src/tools/own_data.rs):

  • JSON arguments: only path-like keys (file_path, path, cwd, workdir, *url, …) and command-like keys (command, cmd, code, args, …), plus a patch file header (*** Update File: …) in any value. content, new_string, description and the like are ignored.
  • Raw tool input (e.g. Codex's freeform apply_patch): a patch file header, or a file command (cat, sed, sqlite3, > …) before the path on the same line.
  • The JSON is read string by string rather than parsed: the wall checks half-received arguments on every fragment, and arguments over its 64 KiB cap are kept only in part. An unterminated last value counts only once the directory name is followed by a separator (~/.thinkwatch may still become ~/.thinkwatch-backup).

Like the other code checks it is not in the client-config scan (that scanner only reads re).

Follow-ups outside this repo

  • ThinkWatch Lite: Chinese name, why and matcher text for the rule (UI falls back to the English name and "Decided by a built-in check" until then).
  • Enterprise web: name and matcher text in web/src/i18n and check-i18n.mjs, with the next core bump.
  • Website Lite features page: mention the rule once a release includes it.

Tests

cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings, cargo test --workspace (2680 passed). New: own_data unit tests (Claude Code / Codex / Gemini-style arguments, Windows and server paths, mentions that must not fire, half-received and truncated arguments, escapes), two streaming wall tests (a path split across fragments is cut on the fragment that completes it; an Edit whose text mentions the path passes), and the rule's presence, level and builtin matcher in the tool-inspection view.

🤖 Generated with Claude Code

… say what protects config.yaml

A new built-in tool rule, `thinkwatch-data` (high: cut under enforce),
fires when a tool call's path or command points into ThinkWatch's data
directory: `~/.thinkwatch` (any `.thinkwatch` path component),
`%APPDATA%\ThinkWatch`, and `/var/lib/thinkwatch` or `/etc/thinkwatch` on
a server. The directory holds every upstream key in plain text and the
settings of these protections, so reading it takes every credential in one
step and writing it switches the protections off.

It is a code check rather than a regex so that mentioning the path does not
count: in JSON arguments only path-like keys (file_path, path, cwd, workdir,
...) and command-like keys (command, cmd, code, args, ...) are read, plus a
patch file header in any value; raw tool input needs a patch header or a
file command before the path on the same line. The JSON is read string by
string rather than parsed, since the wall checks half-received arguments
and arguments over its cap are kept only in part.

The configuration reference and README now state the local boundary:
0600/0700 keeps out other users, not programs running as the same user;
outbound redaction protects what leaves the machine, not the file; the
control key is masked so a control-plane write cannot change it, not to
hide it from local programs.

Refs #286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant