dsx reads an OAuth credential, fetches bytes from a remote server, and writes files to your disk. Each of those is worth taking seriously, so this document says what dsx claims about them and how to report it when a claim turns out to be false.
Do not open an issue. Use GitHub's private vulnerability reporting:
https://github.com/somework/dsx/security/advisories/new
If that is unavailable to you, email i.pinchuk.work@gmail.com with dsx security in the subject line.
Please include what you measured rather than what you inferred: the command, the input, and what dsx did. A proof-of-concept against a project you own is worth more than a description.
This is a project maintained by one person. You will get an acknowledgement, and a fix or an explanation of why the behaviour is intended — but no service-level promise, because one made here would be worth nothing. Please give it a reasonable window before disclosing publicly.
The latest commit on main, and the most recent release. Nothing older is patched.
These are the properties a report can falsify. Each is enforced by a named test; the reasoning behind them is in CLAUDE.md's invariants.
The OAuth token is read, never written, never printed. dsx reads the credential Claude
Code already stored — the macOS Keychain first, then ~/.claude/.credentials.json — and never
refreshes it, because rotating the refresh token would silently break Claude Code's own login.
It does not appear in output, in errors, or on disk. dsx auth and dsx doctor report scopes
and expiry and are tested against printing the token itself.
The preview URL is a credential and the sync lane never lets one out. render_preview
returns a short-lived *.claudeusercontent.com URL carrying a token scoped to the whole
project and needing no Authorization header — the string is about an hour of read access
to every file in that project. In the sync lane it is minted, used and dropped inside one
function; it reaches no report, no error and no file.
Only two hosts are fetched. The preview lane admits an https host whose Hostname()
ends in .claudeusercontent.com, or the configured endpoint's own scheme/host/port, and
nothing else. Userinfo is refused on both (https://x.claudeusercontent.com@evil.test/ is
evil.test), redirects are not followed, the bearer token is deliberately not sent to the
preview host, and the body is read through a limit reader bounded by the size the listing
reported.
Remote paths and remote bytes are untrusted input. A server-supplied path cannot escape
the synced directory, including through a symlink, and cannot name .git, node_modules or
dsx's own bookkeeping. Comparisons fold case, because APFS does. Server-supplied text that dsx
renders is sanitised too — a control character in a project name would otherwise rewrite
what the terminal already showed.
Files are not silently corrupted or silently destroyed. Every pulled file's decoded length
is checked against the size the listing reported, and a mismatch refuses the write. Every file
lands temp-then-rename, so an interrupted run never leaves a half-written file. --prune
deletes only what the ledger proves was dsx's and unmodified.
Release archives carry build provenance signed by GitHub with a short-lived identity minted for the run — there is no long-lived key anywhere in this project. Once a release exists:
gh attestation verify dsx_<version>_<os>_<arch>.tar.gz --repo somework/dsxThat binds the archive to this repository, this workflow and the commit it was built from.
checksums.txt is attested too, since it is the file people verify against and an unsigned
one answers nothing about who produced it. A checksum says an archive is internally
consistent; it does not say who built it, and for a binary that reads an OAuth credential
those are different questions.
Building it yourself is the other answer, and it needs no trust at all: go install compiles
from source you can read.
- The token, or any part of it, reaching stdout, stderr, a file, an error, or the network
anywhere other than the endpoint's
Authorizationheader. - A
serve_urlescaping the sync lane, or a fetch reaching a host outside the two allowed. - A server-controlled path or filename causing a write outside the synced directory.
- Server-controlled content causing dsx to execute anything, or to corrupt output a caller parses.
- A delete or overwrite of local or remote data that the documented rules say should have been
refused — in particular anything
--pruneremoves without--force, or anything--force-with-leaseoverwrites after the server moved. - Credential files being created or left with permissions wider than the owner.
dsx files previewprintingserve_url, anddsx plan newprintingplan_token. Both are scoped capabilities the tool returns, and printing them is what those commands are for —dsx files put --plandocuments itself as taking the token off the previous command's stdout. Treat that output as short-lived credential output: do not log or persist it, and hand peopleopen_urlinstead. Read as a blanket "never print anything token-shaped", this yields a false finding againstfiles preview; one has been filed and withdrawn already.--forceoverwriting without a precondition. That is what the flag is for, and it is documented as being exactly as dangerous as git's.--force-with-leaseis the safe middle.DSX_TOKENorDSX_ENDPOINTpointing dsx somewhere else. Both are the operator's own input. The ledger refuses to sync a directory bound to a different endpoint, which is the guard that matters; being able to set the variable is not the finding.- Behaviour of the Claude Design endpoint itself. It is Anthropic's, not this project's, and it is undocumented — see PROTOCOL.md. Report server-side issues to Anthropic. A drift that makes dsx unsafe is in scope here; the server's own behaviour is not.