Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Chromium browser extension under `editors/browser`: popup scanner, on-page reveal (badges plus CSS highlights), context-menu scan, per-tab toolbar badge, and optional paste-safe stripping that keeps real emoji ZWJ sequences (both neighbors emoji) and stops page paste handlers from seeing the raw clipboard. Zero remote code; `scan.js` matches the VS Code engine copy. Load unpacked from `chrome://extensions`.
- WASM build of `fuckmark-hidden-scan-v1` (`crates/fuckmark-scan`, committed `fuckmark_scan.wasm`). The scan page and the Chromium popup load it when `fetch` works so classification stays on-device; `scan.js` remains the `file://` fallback. Lone UTF-16 surrogates route through the JS engine (UTF-8 cannot carry them). Empty category arrays stay empty selections. The extension CSP allows `wasm-unsafe-eval` so Chromium can instantiate the module. Page reveal and paste-safe keep the synchronous JS port. Rebuild with `crates/fuckmark-scan/build-wasm.sh`.
- Language bindings for the same Rust scan ABI: C header (`crates/fuckmark-scan/include/fuckmark_scan.h`), optional Python ctypes loader (`fuckmark.native_scan`), and a Node package under `bindings/node` (WASM + JS fallback). Host library build: `crates/fuckmark-scan/build-native.sh`. Lone UTF-16 surrogates cannot cross the UTF-8 ABI, so Python `scan_text` / `clean_text` delegate those strings to the Python reference scanner, and the WASM wrapper normalizes the JS fallback result to the same `index` / `truncated` / `highest_severity` / `source_length` schema. The C example is linked with `-rpath,$ORIGIN` and written next to `libfuckmark_scan.so`. Overview: `bindings/README.md`.
- OS clipboard watcher (`fuckmark-clipboard-watch-v1`). `fuckmark clipboard scan` / `clean` / `watch` poll the system clipboard (pbpaste, wl-paste, xclip, xsel, PowerShell Get-Clipboard) for the same security category set as `fuckmark lint`. `--clean` rewrites the clipboard in place after a digest re-read so a newer copy is not overwritten. Snapshots hash with UTF-8 `surrogatepass` so lone surrogates do not crash the watcher. Linux read uses `wl-paste` without `--no-newline`. Emoji ZWJ sequences (both neighbors emoji) and info-level variation selectors are kept, matching browser paste-safe. `--json`, `-q`, `--select`, `--once`, `--exit-on-find`, `--interval`, `--max-seconds`. Exit `0` clean, `1` findings, `2` usage, `3` clipboard unavailable. Reference: `docs/clipboard.md`. Python: `evaluate_clipboard_text`, `watch_clipboard`, `clean_clipboard_text` (injectable reader/writer for tests).
- `fuckmark normalize` (`fuckmark-normalize-v1`): NFC, optional identifier lookalike fold (UTS #39-inspired subset, not a full confusable map), then strip of the security category set, plus a JSON receipt (`input_sha256` / `output_sha256` / `steps` / `report_hash`). `--clean` remains the blunt strip with no receipt. HTTP: `POST /api/normalize`. Python: `normalize_text`, `skeleton_fold`. Reference: `docs/normalize.md`.
- VS Code / Cursor: `fuckmark.cleanOnSave` (default off) strips hidden Unicode on save.
- Frozen confirmation files, hashes, mix mechanisms, and the published wheel are unchanged. Package version stays 0.4.1.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ The [`editors/vscode`](editors/vscode) extension reveals hidden Unicode inline a

The [`editors/browser`](editors/browser) extension reveals hidden Unicode on the page you are reading. Load it unpacked from `chrome://extensions` (Developer mode). The popup scans pasted text or the current tab, **Reveal** paints `U+XXXX` badges on the page, and **Paste-safe** (off by default) strips hidden characters when you paste into a field while keeping emoji sequences. Nothing is uploaded. The popup prefers `fuckmark_scan.wasm` when it can fetch it; on-page reveal and paste-safe keep `scan.js`. Full notes: [`editors/browser/README.md`](editors/browser/README.md).

## Watch the OS clipboard

Copied text is a common path for Trojan Source and Unicode-tag smuggling. `fuckmark clipboard` reads the system clipboard and uses the same security category set as `fuckmark lint`. `watch` polls for changes; `--clean` rewrites the clipboard in place. Real emoji ZWJ sequences stay intact.

```text
fuckmark clipboard scan
fuckmark clipboard clean
fuckmark clipboard watch --clean
```

Needs a clipboard tool (`pbpaste`, `wl-paste` / `xclip` / `xsel`, or PowerShell `Get-Clipboard`). Full reference: [`docs/clipboard.md`](docs/clipboard.md).

## Guard model input (LLM prompt-injection smuggling)

Hidden Unicode — especially Unicode **tag** characters — can smuggle a second instruction into a prompt that a human reviewer will not see. `protect()` strips that payload before the text reaches a model, and can decode the smuggled ASCII so you can log it. It is not a semantic jailbreak detector.
Expand Down
16 changes: 16 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ fuckmark --clean --file suspect.txt -o clean.txt
fuckmark lint src/
fuckmark guard --json < messages.json
fuckmark normalize --receipt < notes.txt
fuckmark clipboard scan
fuckmark clipboard watch --clean
fuckmark web
fuckmark --text "I don’t agree." --status
```
Expand Down Expand Up @@ -141,6 +143,20 @@ fuckmark guard --refuse --receipt --json < messages.json

`--json` walks every string. `--refuse` exits 1 and writes nothing when hidden Unicode is present. `--report` scans without changing the payload. `--receipt` writes the JSON receipt to stderr. Python: `protect()`, `inspect()`, `Guard`, `HiddenTextRefused`. Reference: [`guard.md`](guard.md).

### `fuckmark clipboard`

Scan, strip, or poll the OS clipboard for hidden Unicode (Trojan Source bidi, zero-width, tag smuggling). Default categories match `fuckmark lint`. Real emoji ZWJ sequences (both neighbors emoji) and info-level variation selectors are kept. Reference: [`clipboard.md`](clipboard.md).

```text
fuckmark clipboard scan
fuckmark clipboard scan --json
fuckmark clipboard clean
fuckmark clipboard watch --clean
fuckmark clipboard watch --once --exit-on-find
```

`scan` reads once. `clean` rewrites the clipboard in place when findings exist. `watch` polls (default 0.5s) and warns when the clipboard contents change to a hidden payload. `--select security|all|CATS` chooses categories. Exit status for this subcommand: `0` clean, `1` findings (including after a successful clean), `2` usage, `3` clipboard tool missing or failed.

### `fuckmark web`

Open the local browser tool (same UI as `docs/mark.html`). Aimed at beginners who prefer a page over pipes and flags. The server also exposes a Python API: `GET /api/health`, `POST /api/remove-marks`, `POST /api/scan`, `POST /api/guard` (sanitize text or chat messages before a model call), and `POST /api/normalize`. Hidden-Unicode reveal is `/scan.html` (WASM engine with `scan.js` fallback; zero-install copy at `docs/scan.html`).
Expand Down
89 changes: 89 additions & 0 deletions docs/clipboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# FuckMark clipboard — scan, clean, and watch copied text

`fuckmark clipboard` sits at the OS paste edge. It reads whatever is currently
on the clipboard, runs `fuckmark-hidden-scan-v1`, and can strip hidden Unicode
before you paste into a terminal, editor, or chat box.

Default categories match `fuckmark lint`: `bidi_control`, `zero_width`, `tag`,
`control`, `noncharacter`, and `surrogate`. That is the Trojan Source /
zero-width / Unicode-tag smuggling set. It does not judge visible text.

This is the desktop twin of browser paste-safe. It keeps real emoji ZWJ
sequences (both neighbors emoji) and info-level variation selectors.

## Command line

```text
fuckmark clipboard scan # read once, report, do not change
fuckmark clipboard scan --json
fuckmark clipboard scan -q
fuckmark clipboard clean # strip findings in place
fuckmark clipboard watch # poll until Ctrl+C
fuckmark clipboard watch --clean
fuckmark clipboard watch --once --exit-on-find
fuckmark clipboard watch --interval 0.25 --max-seconds 30
fuckmark clipboard watch --select all
```

`scan` and `clean` run one clipboard read. `watch` polls every `--interval`
seconds (default 0.5) and warns only when the clipboard *contents change* to a
payload that still has findings. Identical text is not re-alerted. `--once`
exits after the first poll. `--exit-on-find` exits when the first hidden
payload appears. `--max-seconds` stops a watch loop even if nothing was found.
`--clean` re-reads the clipboard immediately before rewriting it, so a newer
copy made during the alert is left alone.

`--select security` is the default. `--select all` includes variation
selectors, enclosing marks, format controls, and private-use. Pass an explicit
comma-separated subset to match `fuckmark lint --select`.

`--json` writes a receipt to stdout (`algorithm_version`, `action`, `removed`,
`scan`). Human reports go to stderr. `-q` writes the `fuckmark-scan` machine
line to stdout.

## Exit status

| Status | Meaning |
| ---: | ---: |
| 0 | No findings in the selected categories (or the watch loop ended cleanly). |
| 1 | Findings were present. `clean` still returns 1 after a successful rewrite. |
| 2 | Usage error (missing subcommand, bad `--select`, non-positive `--interval`). |
| 3 | No clipboard tool, or the tool failed. Nothing was rewritten. |

`fuckmark clipboard` is dispatched like `fuckmark lint`. A file named
`clipboard` in the current directory is not read; use `fuckmark --file
clipboard` for that.

## Clipboard tools

| Platform | Read | Write (via existing CLI copy) |
| --- | --- | --- |
| macOS | `pbpaste` | `pbcopy` |
| Linux | `wl-paste`, then `xclip -o`, then `xsel` | `wl-copy`, `xclip`, `xsel`, `clip.exe` |
| Windows | PowerShell `Get-Clipboard -Raw` | `clip` (UTF-16) |

Install one of those tools on a desktop session. Headless CI has no clipboard;
tests inject a fake reader and writer and never call the OS.

## Python

```text
from fuckmark.clipboard_watch import (
clean_clipboard_text,
evaluate_clipboard_text,
watch_clipboard,
)

alert = evaluate_clipboard_text(copied)
cleaned, removed, _scan = clean_clipboard_text(copied)
code = watch_clipboard(
once=True,
clean=True,
reader=lambda: copied,
writer=sink.append,
)
```

`watch_clipboard` accepts `reader`, `writer`, `sleeper`, and `clock` so tests
and embedders do not need a real clipboard. Algorithm id:
`fuckmark-clipboard-watch-v1`.
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,6 @@ python -m pytest

If the command is not found, use the venv path from the install step, or open a new terminal so PATH updates load.

On Linux, clipboard copy needs `wl-copy`, `xclip`, `xsel`, or `clip.exe`. Stream `--copy` still prints the text if copy fails (exit 3). The paste UI does not print the payload; pipe text if you need stdout.
On Linux, clipboard copy needs `wl-copy`, `xclip`, `xsel`, or `clip.exe`. Stream `--copy` still prints the text if copy fails (exit 3). The paste UI does not print the payload; pipe text if you need stdout. `fuckmark clipboard` also needs a matching read tool (`wl-paste`, `xclip -o`, or `xsel`).

Website: [mark.q1z.org](https://mark.q1z.org). Controlled copy: [`website.md`](website.md).
7 changes: 7 additions & 0 deletions fuckmark/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
scan_human_report,
scan_machine_line,
)
from .clipboard_watch import run_clipboard_argv
from .guard import run_guard_argv
from .lint import run_lint_argv
from .product.normalize import run_normalize_argv
Expand Down Expand Up @@ -231,6 +232,7 @@ def _parser() -> argparse.ArgumentParser:
" fuckmark lint src/\n"
" fuckmark guard --json < messages.json\n"
" fuckmark normalize --receipt < notes.txt\n"
" fuckmark clipboard watch --clean\n"
" fuckmark web\n"
"\n"
"Latin, Greek, Cyrillic, Han, Kana, Hangul syllable, and emoji sites are processed\n"
Expand All @@ -253,6 +255,7 @@ def _parser() -> argparse.ArgumentParser:
"Use fuckmark lint PATHS to scan files/directories and fail on findings (CI, pre-commit).\n"
"Use fuckmark guard to strip hidden Unicode from text or JSON before a model call.\n"
"Use fuckmark normalize to NFC-fold, optionally skeleton-fold lookalikes, and strip hidden Unicode.\n"
"Use fuckmark clipboard to scan, clean, or watch the OS clipboard for hidden Unicode.\n"
"Use fuckmark web to open the local browser tool (beginner-friendly).\n"
"That server also runs the Python detect/strip API.\n"
"Mn-strip, default-ignorable strip, UnicodeSanitizer combinations, and Cf-strip after UnicodeSanitizer leave Me/Cc/Cf residuals and spaces."
Expand Down Expand Up @@ -965,6 +968,10 @@ def _run(
raw_argv = list(sys.argv[1:] if parser_argv is None else parser_argv)
if raw_argv and raw_argv[0] == "web":
return run_web_argv(raw_argv[1:], errors)
if raw_argv and raw_argv[0] == "clipboard":
_ensure_utf8(output)
_ensure_utf8(errors)
return run_clipboard_argv(raw_argv[1:], output, errors)
if raw_argv and raw_argv[0] == "lint":
_ensure_utf8(output)
_ensure_utf8(errors)
Expand Down
Loading