Check us without trusting us

An unsigned executable from a store nobody has reviewed yet deserves suspicion. This page is the answer to that: the checksums of the files we serve, an import-level scan of the source inside the zip, and a string scan of the binary itself. Every figure here was produced by a script reading those exact files — and every one of them can be recomputed on your machine in under a minute.

Checksums

SHA-256 of what the download link actually hands you. Compare after downloading, before running anything.

FileBytesSHA-256
3qpdf.part1.zip21 002 211d2ca809c808c951605d65535d24e601435ae92259b07b86e86ac9fefc9eb9006
3qpdf.part2.zip21 034 1518675f02a9b4b415769aa068f73eeed645abf6a90fe8a085e69386ddcbaa240f7
3Q-PDF-Tool.exe
after joining the two parts
41 902 47030ed7059afc84a6cac24ff20584cfac85e58316bc6cb5f8ac77edea3d617edac
sheetkit.zip73 564ba9b73fa63bfb485567779ab9e515613ab4d353ff4a0a9a4cfde5dc69d32a50f
The executable arrives in two parts. It is 41.9 MB, so the download is two zips that must be unzipped into the same folder and joined by JOIN-AND-RUN.bat. That script checks one thing: that the joined file is 41 902 470 bytes. It does not check the hash. If you want the stronger check, run certutil -hashfile 3Q-PDF-Tool.exe SHA256 and compare against the row above — a size check catches a truncated download, not a swapped one.

What is inside the executable

The binary was scanned string by string, in both 8-bit and UTF-16 form, for anything that looks like a URL. This is the complete result — not a sample.

Bytes scanned%s
Strings found%s ASCII, %s UTF-16
URLs found%d
Distinct domains%d

Every domain in the binary

DomainOccurrences
schemas.microsoft.com1

That one entry is an XML namespace identifier in the Windows application manifest, not an endpoint. It is never contacted.

Strings that were looked for and are absent

NeedleOccurrences
getwold.com0
github.com/weili0
api.waffo0
pancake0
telemetry0
analytics0
mixpanel0
sentry.io0
google-analytics0
posthog0
license_key0
activate.0
updates.0
What this means practically. A program that phones home has to name a host somewhere in its bytes. There is no host in this one belonging to the author, no analytics endpoint, no licence server, no update checker. The way to confirm it behaves that way is the firewall test: block outbound traffic for the process, then use every command you care about. If one step fails, that step tried to leave the machine.

The source that ships with it

Five Python files, 9 967 lines. Parsed into an abstract syntax tree and scanned for network-capable imports — the same check, in effect, as grepping for requests, urllib and socket.

FileBytesLinesNetwork imports
build_exe.py5 551118none
pdf_cli.py37 947953none
pdf_core.py73 2501 806none
pdf_mcp.py44 6401 114http, socket
pdf_tool.py276 9535 976none
pdf_mcp.py:799
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
pdf_mcp.py:805
return socket.gethostbyname(socket.gethostname())
Two socket calls exist, and they are not outbound traffic. The code above is every place a socket is touched. Line 799 opens a UDP socket and connects it to a non-routable address purely so the OS answers "which of my interfaces would carry this" — no packet is sent. Line 805 resolves the machine's own hostname. Both exist for one optional feature: an inbound MCP server, which listens on 127.0.0.1 unless you explicitly pass a different --host.
Correction to something we wrote earlier. This site used to claim that "no port opens, nothing is reachable from the network". That was wrong, and a grep would have caught it: the MCP server has an HTTP transport that opens a port on the loopback interface. The accurate statement is the one above — no outbound connections, one optional inbound listener bound to 127.0.0.1 by default. The page has been fixed.

Shipped source vs public repository

The source in the zip is the same source anyone can read on GitHub. This matters more than the scan: if the two differed, the scan would prove nothing about the binary.

Shipped fileCompared with the public repository
build_exe.pybyte-identical
pdf_cli.pybyte-identical
pdf_core.pyidentical after CRLF → LF
pdf_mcp.pybyte-identical
pdf_tool.pyidentical after CRLF → LF

Compared against 125f5fc7ed6e (2026-10-28T10:13:18Z). Two files were saved with Windows line endings when the zip was built; the characters are otherwise the same. Normalise with .replace(b"\r\n", b"\n") before hashing.

Reproduce it yourself

Four commands. No account, no tooling beyond Python, which you already have if you have any of the things this site sells.

1 · Join the two parts and hash the result

copy /b 3Q-PDF-Tool.exe.part1 + 3Q-PDF-Tool.exe.part2 3Q-PDF-Tool.exe
certutil -hashfile 3Q-PDF-Tool.exe SHA256

Expect 30ed7059afc84a6cac24ff20584cfac85e58316bc6cb5f8ac77edea3d617edac and 41 902 470 bytes. On macOS or Linux, cat part1 part2 > out.exe && shasum -a 256 out.exe.

2 · Scan the binary for hosts

python -c "import re,pathlib;b=pathlib.Path('3Q-PDF-Tool.exe').read_bytes();print(sorted({m.group(1).decode().lower() for s in re.findall(rb'[ -~]{6,300}',b) for m in re.finditer(rb'https?://([A-Za-z0-9._~-]+)',s)}))"

Expect ['schemas.microsoft.com'].

3 · Scan the shipped source for network imports

python -c "
import ast,pathlib
NET={'socket','urllib','http','ssl','requests','httpx','ftplib','smtplib','telnetlib','asyncio'}
for p in sorted(pathlib.Path('source').glob('*.py')):
    t=ast.parse(p.read_text(encoding='utf-8')); m=set()
    for n in ast.walk(t):
        if isinstance(n,ast.Import): m|={a.name.split('.')[0] for a in n.names}
        elif isinstance(n,ast.ImportFrom) and n.module: m|={n.module.split('.')[0]}
    print(p.name, sorted(m&NET) or 'none')
"

Expect none for four files and ['http','socket'] for pdf_mcp.py.

4 · Compare the zip against GitHub

python -c "
import hashlib,pathlib,urllib.request
for f in ['pdf_cli.py','pdf_mcp.py','pdf_core.py','pdf_tool.py','build_exe.py']:
    a=pathlib.Path('source',f).read_bytes().replace(b'\r\n',b'\n')
    u='https://raw.githubusercontent.com/weili02201/3q-pdf-tool/main/'+f
    b=urllib.request.urlopen(u).read().replace(b'\r\n',b'\n')
    print(f, hashlib.sha256(a).hexdigest()==hashlib.sha256(b).hexdigest())
"

All of the numbers on this page are also served as JSON, and can be read through the MCP endpoint's get_product tool under integrity.

What this page does not prove

Being precise about the limits is the point, so:

Generated from the shipped files on 2026-10-28. Re-run build_integrity.py whenever a file changes; the page and the JSON are written by the same script, so they cannot disagree.