Docs
Trust and verification
On the hosted app at reuseforge.com, you can tell who published an item and whether the files you hold are the files that were published. You cannot yet tell that the code is safe. This page says what the platform checks, what it only records, what is planned, and what you still have to check yourself.
Two products share these docs. This page is about the hosted app, which uses accounts, sessions and API tokens. Everything about Ed25519 keys, the signed ledger and attestations applies only if you run the Python node yourself. It is collected in Self-hosted node at the end of the page.
Accounts and sign-in
A participant on the hosted app is an account with a handle, an email address and a way to sign in. Nothing links an account to a legal person. There is no identity check or KYC, and a verified email address shows only that someone can read mail sent to it.
You can sign in with a password, with GitHub, or both. Password sign-in can be protected with an authenticator app. Passwords must be at least 10 characters and are refused if they are on a list of common passwords or contain the name part of your email address. The server stores a salted PBKDF2-SHA256 hash (100,000 iterations), never the password.
Sessions and cookies
- Signing in sets one cookie,
__Host-rf_sid, markedHttpOnly,SecureandSameSite=Lax. It holds a random value. The server stores only its SHA-256, so a copy of the session table cannot be turned back into working cookies. - A session lasts 30 days. An account keeps at most 20 sessions, and the oldest are dropped. Every sign-in gets a new session id.
- Settings, Security lists your sessions with the last time each was used, its IP address and its browser, and lets you sign out one session or all the others. Changing your password signs out your other sessions, and resetting it signs out all of them.
- Requests that change data and carry the cookie must come from the site's own origin, which blocks cross-site forgery.
- Sign-in guessing is limited. After 8 attempts for one email or handle from one address inside 15 minutes, further attempts get a
429until the window passes or you reset your password. An address is also capped at 40 sign-in attempts per 10 minutes. - The same page lists the last 50 security events on your account (sign-ins, password and email changes, two-factor changes, token creation and revocation). Where email delivery is enabled, the platform also emails you about the more serious ones.
Two-factor sign-in
You can turn on time-based one-time passwords (TOTP: 6 digits, 30 seconds) from Settings, Security. Setup asks for your password first, so a stolen session cannot enrol its own authenticator. You get 10 recovery codes once; the server keeps only salted hashes of them, and each works once. A code from the authenticator also works once. More than five wrong codes end the pending sign-in, and the pending state lasts 10 minutes. Turning two-factor off needs your password and a code. Two-factor applies to GitHub sign-in too.
GitHub sign-in
GitHub sign-in uses the authorization-code flow with PKCE and a state value tied to a cookie. It asks GitHub for read:user and user:email. New accounts are created only from your verified primary GitHub email. The platform never links a GitHub login to an existing account just because the email matches: you sign in with your password first and connect GitHub in Settings, Connections. You cannot disconnect GitHub if it is your only way to sign in.
This is separate from importing a repository, which reads public GitHub data without your login. See Where the files come from.
Email addresses and recovery
- Verification links last 24 hours and password reset links last 1 hour. Both work once, and the server stores only their hash.
- Changing your email asks for your password and, where email delivery is enabled, a confirmation link sent to the new address.
- A password reset also verifies the address. If the address had not been verified before, the reset removes any two-factor setup, API tokens and GitHub link that were added earlier, because someone else could have created them by registering the address first.
- Forgot-password answers the same way whether or not the address has an account.
API tokens and scopes
Scripts and agents use API tokens instead of your password. Create them in Settings, API tokens.
- A token starts with
rf_and is shown once. The server stores only its SHA-256, plus the first 10 characters so you can recognise it in the list. - You choose a name, scopes and an expiry of 1 to 365 days (or none). You can hold up to 50 active tokens and revoke any of them at once. The list shows when each was last used.
- Scopes:
read(read your private items and account data),write(publish and edit items, versions, reviews and discussions as you),orders(see your orders and unlock paid files you own) andadmin, which only admin accounts can create or use. - A token cannot change credentials. Creating tokens, changing your password or email, managing sessions, two-factor and deleting the account all need a browser session.
- Anyone holding a token can act as you within its scopes. A
writetoken can publish a new version of every item you own. Give each integration its own token, the narrowest scope and an expiry.
Payments run in test mode and no real money moves, so no token can spend real funds today. The API reference is a placeholder for now; see API reference.
How an item version is identified
An item is a published listing: a repo, skill, harness, MCP server or another kind. The owner publishes versions of it, each with a version number such as 1.2.0 and a set of files (at most 100 files, 512 KB each and 2 MB per version).
The content hash
When a version is published, the server computes the SHA-256 of every file and then a content hash for the version: the SHA-256 of the sorted lines path:file-sha256, joined by newlines. The publisher does not supply the hash, so a publisher cannot claim a hash for different files.
- The Versions tab of an item shows the first 12 characters, prefixed with
sha256:, with a button that copies the full value. The API returns the full value ascontent_hashwith the version list, the file listing and the JSON bundle. - Versions are immutable. Nothing edits the files of a published version, and publishing the same version number again is refused. An owner who made a mistake publishes a new version, or yanks the bad one. A yanked version is marked, no longer counts as the latest and is not installed by default, but it can still be fetched by number.
- File paths are checked: relative, no
..segments, no control or direction-changing characters.
You can recompute the hash from the bundle (replace OWNER and SLUG):
curl -sL -o bundle.json "https://reuseforge.com/api/items/OWNER/SLUG/bundle"
python3 - <<'PY'
import base64, hashlib, json
b = json.load(open("bundle.json"))
lines = sorted(
f["path"] + ":" + hashlib.sha256(
base64.b64decode(f["content"]) if f["encoding"] == "base64" else f["content"].encode()
).hexdigest()
for f in b["files"])
print("computed:", hashlib.sha256("\n".join(lines).encode()).hexdigest())
print("reported:", b["content_hash"])
PYWhat the hash tells you, and what it does not
| It tells you | It does not tell you |
|---|---|
| The files you hold are the files the server stored for that version number. | That the code is safe, works or does what the description says. |
| Whether a version changed since you recorded its hash. Pin the hash when you adopt a version and compare later. | That the publisher wrote the code or has the right to publish it. |
| That a download was not corrupted or truncated. | Anything about a later version. Each version has its own hash. |
The hash is not a signature. ReuseForge computes it and stores it next to the files. It protects you against changes after you pinned it and against damaged downloads. It does not protect you against the platform itself being compromised, because whoever could change the files could change the hash. The platform also does not re-check stored files against the hash on every download. Keep your own copy of the hash if the version matters to you.
The REST API v1 reports its own content hash for a version, with a sha256: prefix and computed over a different line format. The two values do not match each other, so compare a value only with one of the same kind.
Where the files come from
- Published versions carry files stored on ReuseForge, with a content hash.
- Imported GitHub items carry only metadata and the README, copied from a public repository when you import it. The code stays on GitHub, so there is no content hash. The listed install command is a plain
git cloneof the repository, which gives you whatever the default branch holds when you run it, not what it held at import time. The star count shown for the original repository is a snapshot from import time. - Importing does not prove you own the repository. Any signed-in user can import any public repository. Check that the source link, the owner and the repository agree before you trust the listing.
How sellers and items are vetted
Today
| Control | What it does | Limit |
|---|---|---|
| Verified email to sell | Listing a paid item, or changing an item to paid, needs a verified email address. Free items do not. | It shows control of a mailbox, not who you are. Paid listings cannot be bought yet; payments run in test mode and no real money moves. |
| Reserved handles | Brand and route names, and any handle that starts with reuseforge or repoblock, cannot be registered. | This is the only impersonation protection today. A look-alike name is not caught. |
| Reports | Any signed-in user can report an item, account, review, discussion or comment (spam, malware, licence, scam, abuse or other). You cannot report your own content, a second open report on the same target is refused, and each person can file 20 per day. | Reports are read by people. There is no automatic action. |
| Moderation | Admins work through the report queue and can dismiss a report, remove an item or suspend an account. A removal or suspension needs a written note. The reporter gets a notification with the decision, and the owner of a removed item is told the reason. | Review happens after publication, not before. |
| Suspension | A suspended account is signed out, its API tokens are revoked and its active items are archived. An admin can undo it. | It applies to one account. |
| Admin tools | Admins can also feature or remove items and mark an account as verified by staff. Every admin action is written to an audit log. Admin tools need a sign-in less than 12 hours old, and the admin role cannot be obtained through the app: an operator grants it directly in the database. | The staff badge is set by hand and is not a safety guarantee. |
| Rate limits | Per account and hour: 30 new items, 60 version uploads, 20 GitHub imports and 20 reviews. Sign-up is limited per address. | They slow abuse down and do not stop it. |
| Safe handling of content | Files are served with nosniff and a locked-down content security policy. HTML, SVG and scripts are downloaded or shown as plain text, never run on the ReuseForge origin. READMEs go through a Markdown renderer that escapes text first and links only http, https, mailto and in-site paths. Install fields reject hidden and direction-changing characters. | This protects the site and your browser. It does not make the code you download safe to run. |
| On-request static checks | Someone who can write to an item can ask the API for a static check of a stored version. It recomputes the hashes and checks size limits, the manifest, whether the licence is a valid SPDX expression, committed secrets, native binaries and skill or MCP configuration. The result is stored with the version. | Available through the API only, not shown in the web app, and not run automatically. It does not run the item's tests and is not a malware or vulnerability scan. |
ReuseForge does not run the tests of a published item. A test pass rate shown on a listing is not a result the platform produced.
Planned, not built
- Static security scans of published versions, run by the platform rather than on request.
- Impersonation protection beyond the reserved handles above.
Until these exist, treat every item as unreviewed code from a stranger.
What the browser shows you
- Owner. The handle, and a check mark if staff marked the account as verified. Staff set this by hand.
- Licence. A badge with the licence the owner declared, or “No license” and “Not stated” when there is none. Nothing scans the code to confirm it.
- Demo. A badge on sample listings, which show how listings work. They are not real products.
- Source and version. A link to where the code lives, the latest version number, and on the Versions tab each version's content hash, size and a Yanked badge where it applies.
- Reviews, ratings and stars. Written by other accounts. The star count adds the GitHub stars captured at import time. These are signals, not checks.
- Report this item. A button on every item you do not own, for signed-in users.
What you should still check yourself
- Read the code before you run it. A skill steers an agent, a harness runs your agent, and an MCP server runs on your machine with whatever access you give it. Look for network calls, file access and anything that reads environment variables.
- Pin what you adopt. Record the version and its content hash, and compare when you update. For an imported item, pin a commit yourself.
- Check the licence yourself. The badge is what the owner declared. Licence compatibility inside a plan is not checked.
- Check the publisher. Follow the source link, compare the owner with the repository owner, and read the discussions and recent reviews.
- Run unfamiliar code in a throwaway container or VM with no secrets and, where you can, no outbound network.
- Keep tokens narrow. Use
readunless a tool must publish, set an expiry, and revoke tokens you no longer use. - Turn on two-factor sign-in for any account that publishes items other people install.
What is not guaranteed
| You might assume | What is actually true |
|---|---|
| The code works. | Nobody has run it for you. Ratings and stars are opinions and popularity, not test results. |
| The code is safe. | There is no automatic security scan yet. The on-request static check is not one. |
| The licence is right. | It is whatever the owner declared. Nothing scans the code. |
| The publisher is who they say. | Accounts are not tied to real identities. A staff check mark means staff set it, not that the code was audited. Importing a repository does not prove ownership of it. |
| The files cannot change. | A published version is immutable in the app, but the content hash is not signed. For imported items the code is on GitHub and can change at any time. |
| Reviews are independent. | Any signed-in account except the owner can review, and reviews on the hosted app are not tied to a purchase. |
| The money is safe. | Payments run in test mode and no real money moves. Paid listings, escrow, funded builds, orders and earnings are coming soon on the hosted app. The escrow contract described in the docs is unaudited and not deployed. Do not use real funds. See Buying with escrow for the design. |
| Moderation is instant. | Items are reviewed after publication, when someone reports them. |
| Bought code stays private. | Delivered files can be copied. Payment protection is not secrecy. |
| Item text is safe to follow. | Descriptions, READMEs and links come from strangers. The app escapes them and links only http and https, but treat them as untrusted. |
Reporting a problem
- A security flaw in ReuseForge itself. Report it privately, not in a public issue. The Security page explains how, what to include, what to expect and which testing is out of bounds.
- A malicious, infringing or misleading item, account, review or discussion. Sign in and use Report this item or the report option on the page, pick the closest reason and add details. A moderator reviews it, and you get a notification with the outcome.
- A mistaken removal or suspension. Use the contact details in the Terms to ask for a review.
- You think your account is compromised. Reset your password, then sign out your other sessions and revoke your API tokens in Settings.
Self-hosted node
This section applies only to running the Python node yourself (python -m repoblock). The hosted app at reuseforge.com does not use Ed25519 keys, ledger.jsonl, signed events or attestations, and nothing you publish there is written to a ledger. A node calls an item a block; this section says “item” and keeps block names where they are literal commands, event kinds or field names.
A node keeps all state in one append-only log of signed events. That makes the record tamper-evident and attributable to a key. It does not make code good, sellers honest or an unaudited contract safe.
Identities
On a node a participant is a key pair, not an account. A node, a seller, a buyer and an arbiter are each an Ed25519 public key. The public id is rb1 followed by the key in URL-safe base64. A node keeps named identities in keys/NAME.pem: unencrypted PEM files, created with file mode 600 in a directory with mode 700. Identity names are 1 to 32 characters of a-z 0-9 _ -. Keys are pseudonymous: nothing links a key to a legal person.
The signed ledger
Events are stored one JSON object per line in ledger.jsonl. An event looks like this (shortened):
{
"id": "9c1f0a...e2",
"sig": "Qm9v...",
"body": {
"kind": "order.accept",
"author": "rb1HkDai0ooqp6bhUoq4r068YjluVtSBeSapZz9sHtWwig",
"data": { "order_id": "ord_6daf528ce3094372" },
"lamport": 18,
"parents": ["5be1c7...04"],
"ts": 1796000000
}
}sigis the author's signature over the canonical JSON ofbody.idis the SHA-256 of the canonicalbodytogether withsig, so the id changes if any byte changes.parentslists the ids of the events the author had seen at the tip, which links the history together by hash.lamportis a logical clock that orders events without trusting wall clocks.
When a node receives an event it checks the shape and size, that the id matches the content and that the signature verifies for the claimed author. Events that fail are dropped, and one bad event from a peer never blocks the rest.
| Family | Event kinds |
|---|---|
| Registry | block.publish, block.retract, block.attest, review |
| Orders | order.create, order.fund, order.deliver, order.accept, order.dispute, order.resolve, order.cancel, order.ack, order.timeout, order.settling, order.settled |
| Projects | project.create, project.pledge, project.confirm, project.deliver, project.vote, project.transfer, project.product, project.refund |
How nodes agree
This is not a blockchain. There is no mining, no token and no global consensus, because a registry has no double-spend problem. Instead:
- Merge is set union. Two nodes sync by exchanging the events the other lacks (
python -m repoblock sync URL). Whatever the order, both end with the same set. - State is a pure function of the log. The registry, every order and every project are computed by replaying the events in order, and the rules are applied while replaying: only the buyer can accept, only an item's seller can deliver it, only the named escrow agent can confirm a deposit or resolve a dispute. An event signed by the wrong party is ignored, identically on every node.
- Deadlines run on the escrow agent's clock. The
tsof a buyer's or seller's event is informational. Time-driven steps, such as the acceptance window and the delivery deadline, count only when the escrow agent (arbiter) signs them.
What the ledger does guarantee
- Attribution. Every action is signed by the key that did it. Nobody can publish, accept or vote as you without your key.
- Tamper evidence. You cannot edit a past event without breaking its id and signature.
- Role enforcement. The order and project rules above hold on every node that reads the same events.
- Verified purchases. A review counts only if the reviewer's own order for that item was released and settled, and the buyer, the arbiter and every seller on it are different keys. A node can also be set to trust only chosen arbiters.
- Auditability. You can replay the log and reproduce any order's state, payouts and any project's cap table yourself.
Attestations
A block.attest event says: this identity ran this item's tests and got this pass rate. It is signed by whoever ran them, with the block id, the pass rate and the number of tests. The registry replaces the seller's own number with the average of the latest result from each distinct attester. The owner's own attestations never count, and a node can limit the count to an allowlist of attesters.
What an attestation proves, and what it does not. It proves that a particular key said it got that result. By default any other identity can attest to any item, and one person can hold several keys. ReuseForge does not weigh attesters or check that the test suite is meaningful. Treat it as stronger than a seller's claim and weaker than running the tests yourself.
Sandboxing verify
python -m repoblock verify BLOCK_ID clones the item's repository and executes its test command. That is arbitrary code from a stranger, so the command refuses to run until you pass --i-know-this-runs-untrusted-code. It also refuses unless the listing's content_hash is a 40-character git commit, so the code that runs is the code that was published; --allow-unpinned overrides this and runs the tip of the default branch.
What it does for you: it accepts only a plain https git URL on a public host, clones into a temporary directory, passes a minimal environment (PATH, a temporary HOME and CI=1, so your tokens and keys are not inherited), sets CPU-time, memory (2 GB), file-size (64 MB) and open-file limits, enforces timeouts (120 seconds to clone, 300 to test by default), keeps at most 1 MB of output and deletes the folder afterwards. If bwrap is installed, the tests also run with no network and an empty view of your home directory. With only unshare they run with no network but the filesystem is not isolated.
What it does not do: it is not a security boundary. Without bwrap or unshare the test code runs as your OS user, can read any file that user can read and can make network requests.
Run verify only inside a container or a throwaway VM that holds no secrets, and preferably with no outbound network.
One way to draw that boundary, using a scratch copy of the ledger so the container never sees your keys:
mkdir -p scratch && cp ~/.repoblock/ledger.jsonl scratch/
docker run --rm -it -e REPOBLOCK_HOME=/data -v "$PWD/scratch":/data -v "$PWD":/app:ro -w /app python:3.12 \
bash -c "pip install -q -r requirements.txt && python -m repoblock verify BLK_ID --i-know-this-runs-untrusted-code"The attestation is signed by a fresh key and lands in scratch/ledger.jsonl. To bring it into your main node, serve that folder on another port and run sync against it. Adjust the example to your setup. The point is the boundary, not the command.
What a node does not guarantee
| You might assume | What is actually true |
|---|---|
| The delivery matches the listing. | The node records the access link and the hash the seller states. It does not fetch the content or compare it. You do, inside the acceptance window. |
| The seller is who they say. | Keys are pseudonymous. There is no identity verification and no seller staking or slashing yet. |
| The money is safe. | On the simulated rail there is no money. On a stablecoin rail the node operator holds the keys unless you use the escrow contract, which is unaudited and not deployed. See Buying with escrow. |
| The arbiter is neutral. | There is one arbiter per order, by default the node operator. There is no appeal or jury. |
| Nothing can be hidden. | A node can decline to relay events. Merging is union, so honest nodes still converge on what they have seen, but you cannot prove an absence. |
| Self-reported numbers are checked. | Stars and downloads on listings that come from the network are declared by the seller and not independently verified. |
Last updated: