Imported from hyparam/hypaware (
hypaware-core/plugins-workspace/claude/skills/hypaware-privacy/SKILL.md). Install upstream withnpx skills add hyparam/hypaware --skill hypaware-privacy. Copyright stays with the author.
name: hypaware-privacy
description: Audit what HypAware has captured from Claude/Codex sessions on this machine and act on it: survey the recorded directories, sample them for secrets, credentials, and personal content, mark directories (ignore / local-only / sync), and purge sensitive rows. Runs any time. Use when the user says "privacy review", "did I record anything sensitive", "scan my logs for secrets", "what should I hypignore", or wants to see what was captured here. It is also the standard review before an enrolled machine's first org sync: use after hyp remote login prints a first-sync deadline, or when the user says "review before sync" or "what will ship to the server". Covers this machine's local cache only, not rows already forwarded to a remote server.
HypAware privacy review: audit what was captured, decide what leaves
This skill surveys what HypAware has captured on this machine, explains the choices in plain language, and applies the user's decisions through hyp verbs. The six steps run the same way whenever the user asks; only the stakes change.
The one moment they are time-critical is enrollment. When hyp remote login enrolls this machine, the first sync to the org server is held until a printed deadline rather than run immediately, and the whole captured history (backfill included) ships at that deadline unless you refine it first. Doing nothing is a valid choice there - at the deadline everything forwards, which is the documented default. On a machine that was never enrolled there is no pending export at all, and the same steps simply bound what gets recorded and what stays in the local cache.
Run the six steps below in order. Steps 4 and 5 (explain, then confirm) gate every marking: never mark or purge without first explaining the classes and getting per-item confirmation.
Scope honesty (say this up front)
This flow governs HypAware's own surfaces only - what the local cache holds and what the sink forwards. It is not a data-loss-prevention system. Content a user pasted into a synced session, or anything outside HypAware's capture, is out of scope. Be honest that ignore/local-only/purge bound HypAware, not the user's whole machine.
Step 1 - Protect this session first (R3)
The review conversation will discuss the most sensitive content on the machine, so it must never itself become a captured, forwardable transcript. Before surveying anything, opt this Claude session out of capture and verify it took effect. On failure, say so plainly and continue only with the user's explicit consent.
Prefer hyp session ignore --json, which resolves the id and verifies the opt-out in one tested implementation and refuses rather than guessing. It resolves this session's id from CLAUDE_CODE_SESSION_ID, addresses every recorder advertising the control route, validates each reply the same three ways, and fails closed: it exits nonzero and prints no success when it cannot establish the right id, which is the answer this step needs. Only where it is unavailable (command not found) does the script below apply. A hyp that ran and refused is not a fallback: with CLAUDE_CODE_SESSION_ID unset the script exits before it posts anything, and the one id-resolution refusal that happens with it set (a second client stating one too) goes to the stated-id re-run below, not here.
Reading the receipt is not optional. The verb fails closed on the questions it can answer, but two of its successes are narrower than they look, and both are checked below.
hyp session ignore --json
Why the verb and not a curl. Claude Code attaches over OTEL, so this session is recorded by the @hypaware/claude telemetry listener, whose ignored-session set is a different object on a different port from the gateway's. The verb addresses each recorder that advertises control_routes in live daemon status, plus the gateway by its own resolution; one curl reaches one of them, so on the default OTEL attach it can report a confident success over a listener that keeps recording. "status": "ok" means every recorder it addressed took the write, which is not the same claim as "you are covered". So read the receipt rather than only the exit code:
- exit
0with"status": "ok"."status": "partial"(exit 3) means an addressed recorder refused and is still recording. "session_id_source"isclaude_env, so"session_id"is this conversation's ownCLAUDE_CODE_SESSION_ID. Any other source means the verb could not read that variable and resolved a different session off disk instead (codex_rollout/codex_env_rolloutfind a Codex session sharing this directory). It then confirms a real opt-out,"status": "ok"and all, for a session you are not in, while this one keeps being recorded. That write already happened, so undo it before you stop -hyp session unignore "<the session_id it reported>"- or the bystander session stays suppressed until explicitly unignored."recorders"contains an entry forclaude-telemetry, the listener that captures this session. A list holding onlygatewaymeans the listener was never addressed, because a recorder absent from the live daemon snapshot is not addressed at all - and anokover the recorder that was skipped is the exact failure this step exists to prevent. Agateway not addressed:line on stderr narrows the answer the same way from the other side.- every entry in
"recorders"reports"status": "ok"with"ignored": true. Name them to the user rather than saying "the machine". "guarantee": "set_membership"is the bound on all of it, spelled out below.
Stop on any of these and tell the user the review session is still being recorded: "status": "partial", a "session_id_source" other than claude_env (the stated-id re-run just below is the one exception, and reports argument), no claude-telemetry entry in "recorders", or a nonzero exit the fallback does not cover. Only proceed if they explicitly accept that risk.
If the verb refuses because more than one client states an id for this shell (CLAUDE_CODE_SESSION_ID and CODEX_THREAD_ID are both set, so it will not guess which session you are in), do not drop to the script below. hyp is installed and working here, and its own error names the fix: state the id, which still reaches every recorder.
hyp session ignore --json "$CLAUDE_CODE_SESSION_ID"
Read that receipt exactly as above, with one substitution: "session_id_source" is now argument, so check "session_id" equals $CLAUDE_CODE_SESSION_ID byte for byte instead. Every other stop still applies, the claude-telemetry one included.
One nonzero exit, and only that one, sends you to the fallback: hyp not installed (command not found). A hyp that ran and failed any other way is a stop, not a reason to fall back: either it already posted to the gateway the script is about to post to, or it could not resolve a gateway at all - and in that case the script's default address is a guess (resolveGatewayEndpointForCli reads the live daemon port and a pinned listen, and deliberately never assumes the default) rather than a route the verb overlooked.
Fallback, only where hyp is unavailable. The script below posts to the gateway control route directly. It reaches the gateway alone: if a telemetry listener is live, this does not cover it, so report an opt-out of the gateway rather than of the machine, and treat that as the failure the top of this step describes - say it plainly and get the user's explicit consent before surveying.
#!/usr/bin/env bash
set -euo pipefail
if [ -z "${CLAUDE_CODE_SESSION_ID:-}" ]; then
echo "error: CLAUDE_CODE_SESSION_ID is not set; cannot opt this session out" >&2
exit 1
fi
# The default is DEFAULT_GATEWAY_ENDPOINT (src/core/config/gateway_endpoint.js),
# which is what an unpinned gateway binds. On a plain OTEL attach there is no
# ANTHROPIC_BASE_URL at all, so this fallback is the whole of the address.
BASE="${ANTHROPIC_BASE_URL:-http://127.0.0.1:18521}"
URL="${BASE%/}/_hypaware/ignore/session"
response="$(curl --fail-with-body --silent --show-error \
-X POST "$URL" \
-H 'content-type: application/json' \
--data "$(printf '{"session_id":"%s"}' "$CLAUDE_CODE_SESSION_ID")")"
# Verify the gateway accepted the opt-out, and that the reply is about THIS
# session. Same three checks `hyp session ignore` applies (validateControlResponse
# in ai-gateway/src/session_command.js): `ignored` a real boolean true, `total` a
# real number, and `session_id` echoed back byte-for-byte. The route echoes the
# token verbatim, so a reply naming a different session establishes nothing about
# this one, and reaching *something* on the port is not reaching the gateway.
printf '%s' "$response" | python3 -c '
import json, sys
expected = sys.argv[1]
try:
r = json.load(sys.stdin)
except Exception:
sys.exit("opt-out NOT confirmed: the reply was not JSON, so it is not the control route")
# bool is excluded because isinstance(True, int) is True in Python: the CLI
# check this mirrors is `typeof total !== "number"`, which a JSON true fails.
if not isinstance(r, dict) or r.get("ignored") is not True or isinstance(r.get("total"), bool) or not isinstance(r.get("total"), int):
sys.exit("opt-out NOT confirmed: " + json.dumps(r))
if r.get("session_id") != expected:
sys.exit("opt-out NOT confirmed: the reply is about session %s, not %s" % (json.dumps(r.get("session_id")), json.dumps(expected)))
print("opt-out confirmed for session %s (total ignored: %s)" % (expected, r["total"]))
' "$CLAUDE_CODE_SESSION_ID"
If the curl fails (gateway not running, wrong port) or the verification line does not print opt-out confirmed, stop and tell the user the review session is still being recorded. Only proceed if they explicitly accept that risk.
What a confirmed opt-out proves, exactly. Each recorder holds the id as an opaque token: ignored: true means the id is in that recorder's drop set, and nothing more. No recorder inspects traffic, so none can tell you the id is one your exchanges carry - that match happens later, in the client adapter, against the session_id it stamps on the row. For Claude the session is the conversation and CLAUDE_CODE_SESSION_ID is that same id, so sending it is what makes the opt-out real; the reply is a receipt for the write, not a verified drop. Do not report it to the user as more than that, and never treat a follow-up GET as extra proof: it is the same set lookup answering the same question. Nor does anything prove the responder on that port is HypAware, so the answer is only as trustworthy as the machine.
The opt-out is saved locally and survives recorder and daemon restarts until explicitly removed with hyp session unignore. A fork (claude --fork-session, codex fork) creates a new session ID that needs its own exclusion; a plain resume reuses the ID. Transcript backfill honors the saved exclusion. Unignoring permits earlier transcript content to be imported again. Use hyp session status to check the current answer.
Step 2 - Check that backfill has settled (before surveying)
The picker this skill replaces failed because it surveyed a cache the backfill was still filling and presented a partial list as the whole truth. Do not repeat that. Confirm capture has settled before you survey.
hyp status --json # daemon running? enrolled (a central sink present)?
hyp cache status # cache state and last refresh
Then run the enumeration query (Step 3) twice, a short interval apart (say ~30-60s). If the per-directory rows counts are still climbing, backfill is still landing: warn the user and offer to wait until counts stabilize before proposing any markings. Surveying mid-backfill risks marking against an incomplete picture. There is no deadline pressure here: on an enrolled machine the first-sync hold gives hours, and on an unenrolled one nothing is waiting to leave. Note that an enrolled user can also end that window early at any time with hyp sync (it prints what would leave and asks first), so if they say they are in a hurry, finishing the review is what unblocks them, not waiting.
Step 3 - Survey the captured directories, then sample content (R4 applies)
Enumerate the distinct working directories this machine has captured (an enumerate query over ai_gateway_messages):
hyp query sql "SELECT cwd, repo_root, COUNT(*) AS rows, MAX(date) AS last_seen \
FROM ai_gateway_messages WHERE cwd IS NOT NULL \
GROUP BY cwd, repo_root ORDER BY last_seen DESC" --format markdown
Read stderr as well as stdout: a notice:/warning: line reports withheld or stale rows, and an empty stdout is not the same as zero rows. Do not pass --include-local-only - the review works on the not-yet-classified directories, and any directory already marked local-only has already been decided.
Then, for the directories that look worth a closer look (personal paths, unfamiliar repos, high row counts), sample their content looking for:
- credentials and secrets (API keys, tokens, passwords, private keys);
- personal or non-work material;
- candid discussion of identifiable people;
- anything else a person may not want on an org server.
Sample small and read carefully. When you quote a finding back to the user, obey Step 5's redaction rules - even this (opted-out) transcript should stay low-content.
# Example: sample recent content for one directory (adjust the filter/limit)
hyp query sql "SELECT session_id, role, content_text FROM ai_gateway_messages \
WHERE cwd = '<dir>' ORDER BY date DESC LIMIT 40" --format json --output /tmp/sample.json
# then read /tmp/sample.json rather than flooding context via stdout
Step 4 - Explain the three classes (before the first marking, R5)
Before you propose or apply anything, explain the classes in plain language, including what the org can and cannot see in each case:
- ignore (
hyp privacy set <dir> ignore): never recorded going forward; the machine-local rule stops capture at the source. Existing cached rows are purgeable (Step 6) but are not removed by marking alone. The org sees nothing from this directory. - local-only (
hyp privacy set <dir> local-only): recorded and queryable here on this machine, but never forwarded. Withheld at the export seam. The org sees nothing, while you keep local history. - sync (
hyp privacy set <dir> sync): the explicit "this ships" choice - forwarded to the org server like the default. Marking itsyncrecords an explicit decision so this directory is not asked about again. The org sees this directory's captured exchanges.
Name the trade honestly: local-only keeps your history usable locally; ignore is stronger (nothing is even recorded once marked) but you lose local queryability too.
Step 5 - Propose findings as redacted excerpts, confirm per item (R4, R6)
Present findings as short, redacted excerpts and a proposed class per directory:
- Mask credential bodies - show that a key was found and where, never the key itself (e.g.
AWS key ...XY7Q in <dir>/notes.md). - Prefer naming files and directories over reproducing content.
- Keep excerpts short. Even an unprotected transcript should stay low-content.
Then apply nothing without per-item user confirmation. Propose, wait for a yes on each item, then mark. Do not batch-apply.
Keep the response tight: a clear list of candidate directories with the proposed class for each, minimal prose, no restating of the steps. Flag individual sessions separately only when a directory is otherwise fine but one session is not.
Step 6 - Apply only via hyp verbs, and offer purge for every ignore (R6, R7)
Apply each confirmed decision only through the hyp verbs below. Never author policy files or write anything into the user's repositories - the machine-local store is the only target.
hyp privacy set <dir> ignore # class: ignore (stop recording this dir)
hyp privacy set <dir> local-only # class: local-only (record here, never forward)
hyp privacy set <dir> sync # class: sync (explicit "this ships")
hyp privacy show <dir> # report the governing source + class, and residual cached rows; never writes
hyp privacy unset <dir> [class] # remove markings (class-neutral by default; a trailing class scopes it)
hyp privacy show <dir> names which source governs (a committed .hypignore dotfile vs a machine-local entry) and the entry's class, and reports how many already-cached rows still sit under it - the residue that purge (below) clears. Marking is always non-destructive: it changes future capture/forwarding, not existing cached rows.
For every directory you mark ignore, and every session you flag as sensitive, offer hyp privacy purge as a separately confirmed step so that "completely ignored" also means "not sitting in the cache". Purge is destructive and cache-only (it never contacts the server); confirm each purge on its own.
hyp privacy purge <dir> # delete cached rows for a directory subtree
hyp privacy purge --session <id> # delete all cached rows for one session (cheapest: session is the partition key)
hyp privacy purge --ignored # sweep every cached row whose cwd currently resolves to `ignore`
Purge prompts for confirmation on a TTY; it errors on a bare hyp privacy purge with no target. Sequencing matters: mark the directory ignore first, then purge - purging a directory that still resolves to sync/default warns that the next backfill will re-import it. Once a directory is ignored, the capture seam blocks re-import, so the purge is durable. A common close-out for a directory the user wants fully gone:
hyp privacy set <dir> ignore && hyp privacy purge <dir>
After the review
- Nothing you did contacts the server. If this machine is not enrolled, nothing is scheduled to leave it at all, and the markings just bound future capture and what the local cache keeps.
- On an enrolled machine, at the deadline - or sooner, if the user runs
hyp syncand confirms the prompt - the hold expires and export begins:ignored data was never recorded (or was purged),local-onlyrows are withheld at the export seam, and everything else - thesyncdirectories and anything left at the default - ships, backfill included. - Check the pending deadline any time with
hyp status(it shows the first-sync deadline while the hold is live). - Re-running this skill later is safe and idempotent; already-decided directories drop out of the survey.
- New folders the user has not marked sync without asking (the default). If they want to be asked once per new folder instead,
hyp privacy folders askturns that on andhyp privacy folders syncturns it back off. It moves the question only - every directory marked here keeps its class either way.