Privacy, cost & limitations

September 1, 2026 Ā· View on GitHub

For anyone deciding whether to trust Tacit with their conversations. Everything below is the actual behaviour of the code on main; where it has a rough edge it says so.

What stays on your machine

Everything Tacit keeps lives under ~/.dsh/storages/tacit/ (or $DSH_HOME/storages/tacit/). Files are written atomically (write a temp file, then rename) and never truncated in place.

FileHolds
profile.jsonmistake patterns with trust counters, directives (max 8 global + 4 per workspace, each with the normalised absolute workspace path it is scoped to, if any, and its provenance: updatedAt, version, the ids of the reports it was distilled from, the ledger run id of that distillation, evaluatedAt and approvedAt), when a session was last seen in each of those workspaces, the last 6 retired or removed directives so the distiller is not told to propose them again (their own sentence, ids and counters; no prompt text), style rules (max 6), the last 10 šŸ‘Ž verdicts, counters
reports/<conversation>/<turn>.jsonone analysis report per analyzed turn (problems, improved prompt, explanation, a 200-char excerpt of the prompt, your correction if any, the absolute workspace directory of the conversation)
config.patch.jsonthe settings you changed in the UI
auto.jsontoday's date and how many automatic analyses were spent
usage/<YYYY-MM-DD>.jsonone day's usage ledger: runs (one bootstrap batch, auto-analysis, ✨ Improve, distillation, ...), each with its attempts (op, timing, model, provider, token counts, finish reason/code, the attempt's status, priced USD) — never prompt or response text
usage/summary.jsonrolling lifetime / by-type / by-model / by-provider / by-trigger / by-day usage totals, kept alongside the day files so nothing has to re-scan them; every bucket also carries failedCalls and failedUsd, the count and the cost of calls that were billed but whose attempt failed

The turn digests themselves are not here — they live in the harness's own session projection store, next to the session.

What is sent to the model

Every call goes through the harness's model service with your configured key — Tacit never reads, stores or forwards the key. Only the model id is allowlisted (deepseek-v4-flash / deepseek-v4-pro); the provider route is the one your session already uses, so a proxy or self-hosted route configured in the harness is honoured. Nothing is sent anywhere else.

CallWhat goes outClipped to
Analysisprevious turn's prompt + answer; the analyzed prompt; step/tool/error/retry/token counters; the first 25 tool calls (name + arguments); the final answer; your correction600 + 600 Ā· 4000 Ā· — Ā· 400 per call Ā· 3000 Ā· 1000 chars
Directive distillationyour top 12 patterns (kind + one example); the last 5 "prompt → correction" pairs; your style rules; the current directives200 Ā· 120 + 200 Ā· 300 Ā· 220 chars each
Style-rule distillationyour last 3 šŸ‘Ž reasons, verbatim300 chars each
✨ Improveyour draft; the last 2 turns' prompts and answers; style rules; last 3 šŸ‘Ž reasons; trusted patternsdraft as typed (≤ 100 000) Ā· 600 + 600 each Ā· 300 Ā· 300 Ā· 200
Pre-send context (opt-in)your draft; enabled directives; top 6 patterns; the last 2 turns1500 Ā· 220 Ā· 160 Ā· 600 + 600

Everything above that comes from a past turn — prompts, answers, tool arguments, the recent conversation — is read from the turn digest, and the digest is masked at capture: a credential-shaped string is already [redacted:...] before any of these calls is assembled. The one text that is not is the draft you are typing right now, which ✨ Improve and pre-send context send as typed.

Never sent: tool results or file contents (only whether a tool errored), API keys, session ids, anything outside the clips above.

Guarantees

  • No telemetry. Tacit sends no telemetry or analytics of any kind. The only network traffic it produces is the model call through the harness's own model service. A weekly log-audit workflow scans this repository's own CI logs for credentials, personal paths and addresses.
  • Never reads or stores API keys. Calls use ctx.llm.stream, the harness's own service.
  • Credential-shaped strings are masked at capture. API keys, tokens, JWTs, private-key blocks and key=value secrets in a prompt, a tool argument or an answer are replaced with [redacted:...] in the turn digest, so nothing downstream (reports, corrections, any model call) sees them; the list of shapes is in lib/redact.js.
  • Directives never change policy. The steering section states that it does not alter tool permissions, approvals or the sandbox, and a directive that talks about those is rejected, whether distilled or typed.
  • Visible steering. The exact injected text is in Settings → Tacit → Exact text injected, and steerAgent: false removes it entirely.
  • Append-only pre-send. enrichPrompts (off by default) appends a separate, labelled, plugin-sourced message; your words are never rewritten.
  • Same-origin only. The harness web server has no origin policy, so Tacit's routes check Sec-Fetch-Site, Origin and Content-Type themselves: a web page you happen to visit cannot plant a directive or spend your budget through 127.0.0.1. (A request with none of those headers — curl, the smoke test — passes by design.)
  • Three deletion paths, all restricted to Tacit's own files. Clear all analysis reports removes reports/*/<turn>.json files (and the emptied folders). Day files older than costHistoryDays are deleted automatically, at most once per calendar day. Clear usage history resets the whole ledger at once. All three only ever unlink files matching Tacit's own naming (reports/*/<n>.json or usage/<YYYY-MM-DD>.json); the usage/ directory, summary.json (rewritten fresh, not unlinked), the profile and the config are never touched by any of them.
  • Bounded everything. Every text sent to the model is clipped (table above); directives ≤ 220 chars, the steering section ≤ 1400 chars, 8 directives, 12 patterns.

Cost

Every Tacit model call is metered from the model's own usage block (uncached input, output, cache-read, cache-write and reasoning tokens) and folded into a content-free ledger under usage/. Reasoning tokens are always a subset of output tokens, never a separate quantity, so they are never billed twice.

Runs and attempts. The ledger groups calls into runs — one bootstrap batch, one auto/manual/batch analysis, one ✨ Improve, one directive distillation, one style-rule distillation, one pre-send enrichment — and each underlying model call inside a run is an attempt. A run carries an id, its type, a trigger, start/end times, a derived status — success when no attempt failed (an unmetered attempt still counts as not failed), partial when some attempts failed, failed when every attempt failed or there are no attempts, running while open — the session id and turn, the workspace label (never the path), the model/provider and its list of attempts. An attempt carries an id, the op (analysis, analysis-repair, directive-distillation, style-distillation, improve, improve-repair, enrichment), its timing, model, provider, reasoning effort, finish reason/code, a status (ok / failed / unmetered), the session id and turn, the five raw token buckets, and the priced result (null when nothing matched). None of it is ever a prompt, a response, a tool argument or an API key.

The tiles and the run list count differently on purpose. A tile is summed from the per-day totals, which count each attempt on the day it was billed. A run row lives in the day file of the day the run started. So a run that begins before midnight and finishes after it puts tokens into two days' tiles, and it is listed under today as long as any of its attempts happened today. That is the reading that makes the today tile and the today list agree on the same work.

Pricing sources and resolution order. Each attempt is priced once, at the call's own start time, in this order:

  1. dsh-cost-meter's own model rates for that model, when the call went through an official DeepSeek route — tiered off-peak/peak using dsh-cost-meter's own peak windows / effective-at / peak-enabled flag when it supplies them, with Tacit's own Beijing-weekend rule always applied on top (see Tiers below) — dsh-cost-meter has no such rule of its own, which is exactly why the two plugins' figures can differ on a weekend (Known limitations).
  2. dsh-cost-meter's provider rates for that provider + model, when the above did not match — a flat rate, no tier, so a proxy or custom route can still be priced when the meter knows its rate card.
  3. The bundled DeepSeek V4 list price (dated 2026-08-22), for an official route and an allowlisted model, when neither of the above applies.
  4. Otherwise: no price at all — see Unpriced calls below.

dsh-cost-meter's rates always win when they match; the bundled table is the fallback, never a ceiling. The plugin is entirely optional and duck-typed: Tacit never blocks a model call on it, and a getState() call that hangs, throws or returns nothing usable falls back to the bundled table within 5 seconds, remembering why (shown in the Pricing card).

Formula. For one attempt:

cost = (uncachedInput Ā· cacheMissRate
      + (cacheRead + cacheWrite) Ā· cacheHitRate
      + output Ā· outputRate) / 1,000,000

in USD per 1M tokens.

Tiers. Peak hours are 01:00–04:00 and 06:00–10:00 UTC; every other UTC hour is off-peak. Since 2026-08-22T16:00 UTC, Beijing-calendar weekends (Saturday/Sunday at UTC+8) are always off-peak regardless of the hour. The tier is decided once, at the call's start time, so a call that straddles a tier boundary is still priced consistently.

Unpriced calls. A call on a route Tacit can't match to any price table — a proxy or custom provider neither the bundled table nor dsh-cost-meter know — has no computed price; it's counted separately as an unpriced call (shown in the Usage card) rather than assumed free. A call that never received a usage block at all (the adapter reported none) is unmetered, which is never shown as $0.00.

Every call is also made with low reasoning effort and a tool schema (no prose to parse), and every one is tagged with the session id, so a cost plugin such as dsh-cost-meter can additionally show the real spend next to the conversation.

CallWhenMax output tokensCounts against the daily capTagged with sessionEst. cost, deepseek-v4-flash*
Analysis — auto / correctionmessy turn or correction3000yes (30/day)yesā‰ˆ $0.001 off-peak Ā· $0.002–0.003 peak
Analysis — good (learnFromGood)a clean turn right after a messy one3000yes (same 30/day)yesā‰ˆ $0.001–0.002 (shorter answer than a diagnosis)
Analysis — manual / bootstrapyou click3000noyessame; bootstrap (20 + 1 distillation) ā‰ˆ $0.02–0.05
Repair retryanalysis or Improve returned unparseable JSON (rare)same as the callnoyesdoubles that one call
Directive distillationevery 3 analyses1500noyesā‰ˆ $0.0005–0.001
Style-rule distillationevery 3 šŸ‘Ž with a reason1000noyes< $0.001
✨ Improveyou click1500noyesā‰ˆ $0.001–0.002 (its system prompt is a stable, cache-hit prefix)
Pre-send context (opt-in)every send with an 8–1500-char draft1000noyes< $0.001 each, but on every send

* Computed from DeepSeek's list prices (per 1M tokens) with a typical analysis of ~2500 input and ~800 output tokens (reasoning tokens count as output; the other calls are smaller):

ModelCache hitInput (cache miss)Output
deepseek-v4-flash$0.007 off-peak Ā· $0.014 peak$0.22 off-peak Ā· $0.44 peak$0.66 off-peak Ā· $1.32 peakdefault
deepseek-v4-pro$0.022 off-peak Ā· $0.044 peak$0.66 off-peak Ā· $1.32 peak$1.98 off-peak Ā· $3.96 peak3Ɨ flash

Cache-hit input is ~30Ɨ cheaper ($0.007–0.044), and Tacit's system prompts are stable, so real spend is often below the estimates. Peak/off-peak hours and current prices: DeepSeek pricing.

What the Usage card breaks down. Spend on failed or repair calls is shown as one figure. A repair retry that itself failed is both a failed call and a repair call, and the figure counts it once, not twice. Spend by route separates an official DeepSeek route from a proxy or self-hosted one. Spend by trigger separates auto, correction, good, manual and bootstrap runs. The input and output token split is on the tiles as well as the run rows. Today's automatic analyses are shown against the daily cap (autoDailyBudget), beside today's spend.

Guards: automatic analyses are capped per local calendar day (autoDailyBudget); each turn is analyzed at most once automatically; bare continuations are skipped without a call; turns that finished before the plugin started are ignored.

Known limitations

Honest list — these are deliberate behaviours, not hidden surprises:

  • Steering is frozen per conversation. A verdict, an edit or a new directive applies to conversations started afterwards; the running one keeps what it started with. Removing a directive or switching one off is the exception, and re-freezes every open conversation at its next system-prompt assembly.
  • Trials are a trend check, not an A/B test. A candidate is judged only on turns from conversations whose frozen steering text actually contained it; conversations started before it existed (or before a restart) contribute nothing, so a trial can take more than directiveTrialTurns turns of wall-clock time to conclude.
  • Corrections are detected by a heuristic. A turn counts as corrected when your next message matches the correction markers (How it works, §3); a correction phrased differently is missed, and a conversation's last turn is never counted as corrected.
  • "Messy" means two slightly different things. Auto-analysis also counts 15+ steps as messy; the trend and the trial verdict do not (long-but-successful work is never held against a directive).
  • Only automatic analyses are capped. Manual, bootstrap, ✨ Improve, both distillations and pre-send context run outside autoDailyBudget.
  • The trend only sees loaded conversations, and only turns within maxKeptTurns; it needs 40 finished turns to show.
  • Bootstrap runs a small worker pool (bootstrapConcurrency, 1–4, default
    1. and ignores the daily cap.
  • The distiller sees workspace names (the last path segment, e.g. dsh-tacit), never full paths; full paths stay in the local reports and profile.
  • A weekly digest is not implemented yet.
  • Bundled prices are a snapshot, dated PRICES_AS_OF (currently 2026-08-22). A DeepSeek price change is only reflected once this package updates, or via dsh-cost-meter.
  • dsh-cost-meter has no weekend rule. Its price table carries no Beijing-weekend off-peak override, so on a weekend a bundled-table figure and a dsh-cost-meter figure for the same call can differ.
  • The measured bootstrap estimate uses only day files on disk. It reads the priced attempts already written to disk, not an in-flight run's own live counters. A run that has finished is already there — endRun writes its day file synchronously — so only a run still in flight lags: its attempts do not move the estimate until the debounced flush (250 ms) has written them.

← How it works Ā· Next: Configuration