Please report security issues privately through GitHub's private vulnerability reporting on this repository's Security tab, rather than opening a public issue.
If that route is unavailable to you for any reason, open a regular issue saying only that you have a security report and how to reach you — no details — and you'll get a private channel back. Never put the details in a public issue.
Include what you did, what happened, and what you expected — a reproduction is the most useful thing you can send. Please allow time for a fix before disclosing publicly.
field-docket is in early development and cuts no tagged releases yet, so the supported surface is the current main branch. Fixes land there; there is no backport channel.
field-docket is the unusual case among small MCP servers: it exists to keep data, so the storage properties are the security surface.
- Observations are stored unencrypted, on the local filesystem. The store is a SQLite database under
$XDG_STATE_HOME/field-docket/(falling back to~/.local/state/). The directory is created0700and the database file0600, and the-wal/-shmsidecars inherit the database's mode at creation. Those modes are set at creation and are not repaired afterward, so a store that arrives another way — restored from a backup, or made by hand — keeps whatever permissions it came with, sidecars included. Filesystem permissions are the only confidentiality boundary — there is no encryption at rest and no passphrase. - The consequence: treat an observation as readable by anything running as your user. The tool description instructs callers to write an observation in their own words and to keep credentials, tokens, and raw error payloads out of it. That is guidance to a calling model, not an enforced filter — the server cannot inspect the meaning of a free-text string.
- Recorded observations cannot be deleted in-band.
BEFORE UPDATEandBEFORE DELETEtriggers reject row mutation, and no tool exposes a delete. This is deliberate: a store that can quietly drop a record is not evidence. It also means an observation that captured something sensitive needs an out-of-band remedy — see Redaction below. - Nothing leaves the machine. The server opens no network listener, makes no outbound request, and sends data nowhere except back to the caller that asked for it. It speaks MCP over stdio as a subprocess of the calling agent; stdout carries only the JSON-RPC protocol stream, and diagnostics go to stderr.
- Backups are the operator's decision, and they move the boundary.
field-docket snapshot <path>writes a consistent single-file copy at mode0600. The mode is set explicitly rather than inherited: SQLite creates aVACUUM INTOdestination at its own default (0644), unlike the-wal/-shmsidecars. But permissions travel no further than the machine — wherever that copy is sent carries the full corpus, including thescope_refvalues, which are project identifiers and so are metadata even when the observation text is generic.
The append-only triggers block every in-band caller, not every process. DROP TRIGGER is ordinary SQL, so a local operator can remove them, delete the offending row, and recreate them. That escape is intentional — a leaked credential needs a known remedy — but it means the guarantee is "no accidental mutation", not "no mutation". Do not read the triggers as tamper-proofing.
The store runs in WAL mode with synchronous=NORMAL. That is crash-safe against process death but can lose the most recent committed transactions on a power loss or kernel panic. The trade is deliberate — one fsync per observation buys little for this workload — but it is worth knowing in a store whose value is its record.
WAL also coordinates readers and writers through a shared-memory file, which requires every process using the database to be on the same host — SQLite documents that WAL does not work over a network filesystem. Do not place the store on NFS, SMB, or a sync-service folder, including via a symlink.
field-docket holds no credentials of its own. It authenticates to nothing, reads no token, and has no configuration slot for one. Its access is its own process's filesystem access.
- Dependencies and the standard library are scanned for known vulnerabilities.
govulncheckruns on every pull request, on pushes tomain, and weekly on a schedule. It reports advisories whose vulnerable symbols are reachable from this code. The scan is advisory, not blocking — it is not a required status check, so it informs review rather than gating merges. - Build-time dependencies are checksum-verified. Go modules are verified against the checksum database and
go.sum, re-hashed in CI bygo mod verify, and held tidy bygo mod tidy -diffso entries no longer required cannot linger.govulncheckitself is ago.modtool dependency, so the scanner is covered by the same verification as everything else. - CI pins its own toolchain;
mise.lockcovers local development. Tool versions live inmise.tomlwith their resolved download URLs and checksums in the committedmise.lock, andmise installverifies against it — but no CI job installs through mise, so the lockfile governs a contributor's machine rather than the build. CI pins independently: Go comes fromgo.modviaactions/setup-go, golangci-lint and GoReleaser from their actions' explicitversion:inputs. Read the lockfile as reproducibility for local work, not as a control on the published artifacts. - Actions are pinned by commit digest. Every
uses:in every workflow names a full commit SHA with the version as a trailing comment — a tag can be repointed, a commit SHA cannot. - Update automation covers Go modules and GitHub Actions. Dependabot watches both weekly, and bumps the action SHAs along with their version comments.
- The mise toolchain is reviewed manually. No update bot covers
mise.toml, so the Toolchain currency workflow checks the pins against upstream weekly and fails when any is behind. The deliberate failure is what surfaces the report at all, sincemise outdatedexits 0 whether or not a pin has moved. A red run therefore means one of three things, and its annotation says which: a pin is behind and is named, or mise could not report, or its output could not be processed. Read the last two as the reporter having failed rather than as a verdict about the pins — and note that the report is not able to detect a change in mise's own output schema, which would make it fall quiet rather than loud, so a run that stops failing across a mise upgrade is worth distrusting. GitHub notifies you about workflow runs you triggered, on every completed run unless you select the "only notify for failed workflows" option, and for a scheduled run the person notified is the workflow's creator — or whoever last changed its cron or re-enabled it. Keeping this report in its own workflow rather than alongside the scan above is what lets a red result name which of the two fired, since the run list identifies a run by its workflow. It blocks nothing (neither workflow is a required check).just toolchain-outdatedruns the same query locally, against whatever mise is on yourPATHrather than the pinned one, and it reports without failing. The pinned version of mise itself lives in that workflow rather than inmise.toml, so nothing reports it; it moves only when a maintainer moves it. - Scheduled scanning stops firing in a quiet repository. In a public repository GitHub disables scheduled workflows after 60 days without repository activity. That is one repository-level clock rather than one per workflow, so Vulnerability scan and Toolchain currency stop together — read a gap in either one's weekly runs as a symptom, not as a clean result. Re-enabling one moves its notifications, per the bullet above.
This describes a detector paired with a human response, not a project that is continuously free of known advisories. Go patch releases routinely fix reachable standard-library symbols, so the scan going red is expected periodically and is resolved by a maintainer bumping the pinned toolchain.
Release artifacts are built by GoReleaser on a pushed v* tag. The checksums file is signed with cosign keyless signing — no managed keys; the certificate is bound to the release workflow's OIDC identity, so verification uses --certificate-identity-regexp and --certificate-oidc-issuer. SLSA build provenance is attested over the published archives and checksums.
The binary is built with CGO_ENABLED=0. The SQLite implementation is modernc.org/sqlite, a pure-Go translation rather than a cgo binding — so a release carries no C toolchain into the build and links no system SQLite. That choice is what makes the cross-compiled matrix buildable at all, and it also means SQLite advisories reach this project through a Go module version rather than through the host's system libraries.