Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/binary-person/rammerhead/llms.txt

Use this file to discover all available pages before exploring further.

When a browser navigates through Rammerhead, the destination URL appears in the address bar, browser history, and server access logs. URL shuffling replaces the readable URL with an opaque encoded form that is meaningless without the session’s private dictionary. The encoding is applied per-character and is position-dependent, so the same URL looks different in every session.

Why URL shuffling exists

Without shuffling, any URL you visit through the proxy is visible in plaintext to:
  • The server’s access log
  • Browser history
  • Browser extensions that read location.href
  • Anyone who can see the address bar
With shuffling enabled, the address bar shows something like _rhsQ3nR2xBmT9... instead of https://example.com/path. The actual destination is only recoverable by someone who knows the session’s shuffleDict — a 64-character randomly ordered string generated at session creation time.

The base dictionary

Only characters that are safe to embed in a URL path without percent-encoding are eligible for shuffling. The set is derived from printable ASCII (U+0020–U+007E), minus /, _, and any character that would be percent-encoded by encodeURI:
const baseDictionary = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-';
That gives 64 characters. Any character not in baseDictionary — including spaces, Unicode, and most punctuation — is passed through the shuffler unchanged.

Generating a per-session dictionary

At session creation, StrShuffler.generateDictionary() produces a random permutation of baseDictionary:
const generateDictionary = function () {
    let str = '';
    const split = baseDictionary.split('');
    while (split.length > 0) {
        str += split.splice(Math.floor(Math.random() * split.length), 1)[0];
    }
    return str;
};
The result is a 64-character string where every character from baseDictionary appears exactly once, in a random order. This dictionary is stored as session.shuffleDict and is included when the session is serialized to disk.

The shuffle algorithm

StrShuffler takes the session dictionary in its constructor. The shuffle method encodes a URL string:
class StrShuffler {
    constructor(dictionary = generateDictionary()) {
        this.dictionary = dictionary;
    }
    shuffle(str) {
        if (str.startsWith(shuffledIndicator)) {
            return str;  // already shuffled — return as-is
        }
        let shuffledStr = '';
        for (let i = 0; i < str.length; i++) {
            const char = str.charAt(i);
            const idx  = baseDictionary.indexOf(char);
            if (char === '%' && str.length - i >= 3) {
                // percent-encoded sequences are passed through unchanged
                shuffledStr += char;
                shuffledStr += str.charAt(++i);
                shuffledStr += str.charAt(++i);
            } else if (idx === -1) {
                // character not in baseDictionary — pass through unchanged
                shuffledStr += char;
            } else {
                // position-dependent substitution
                shuffledStr += this.dictionary.charAt(mod(idx + i, baseDictionary.length));
            }
        }
        return shuffledIndicator + shuffledStr;
    }
}
The key line is:
shuffledStr += this.dictionary.charAt(mod(idx + i, baseDictionary.length));
For each character:
  1. Find its index (idx) in baseDictionary.
  2. Add the character’s position (i) to create position-dependence.
  3. Wrap around with modulo (64) and look up the result in the session’s dictionary.
Because position is part of the calculation, the same character maps to a different output symbol depending on where it appears in the string. This means statistical attacks based on character frequency are ineffective.

The _rhs prefix

Every shuffled string is prefixed with _rhs (the shuffledIndicator):
const shuffledIndicator = '_rhs';
This prefix serves two purposes:
  • Idempotency: shuffle() checks for the prefix at the start and returns the string unchanged if it is already shuffled. This prevents double-encoding.
  • Detection: unshuffle() checks for the prefix to know whether decoding is needed. A string without _rhs is returned as-is.

The unshuffle algorithm

unshuffle is the exact inverse:
unshuffle(str) {
    if (!str.startsWith(shuffledIndicator)) {
        return str;  // not shuffled — return as-is
    }
    str = str.slice(shuffledIndicator.length);
    let unshuffledStr = '';
    for (let i = 0; i < str.length; i++) {
        const char = str.charAt(i);
        const idx  = this.dictionary.indexOf(char);
        if (char === '%' && str.length - i >= 3) {
            unshuffledStr += char;
            unshuffledStr += str.charAt(++i);
            unshuffledStr += str.charAt(++i);
        } else if (idx === -1) {
            unshuffledStr += char;
        } else {
            unshuffledStr += baseDictionary.charAt(mod(idx - i, baseDictionary.length));
        }
    }
    return unshuffledStr;
}
The subtraction (idx - i) undoes the addition from shuffle. The mod helper uses the positive-modulo formula ((n % m) + m) % m to keep the result in range when the subtraction goes negative.

Percent-encoded characters

Any %XX sequence is passed through both shuffle and unshuffle without modification. This is critical because percent-encoding is how browsers represent characters that are otherwise illegal in a URL (spaces as %20, etc.). Shuffling these sequences would produce an invalid URL.
if (char === '%' && str.length - i >= 3) {
    shuffledStr += char;
    shuffledStr += str.charAt(++i);  // hex digit 1
    shuffledStr += str.charAt(++i);  // hex digit 2
}
The guard str.length - i >= 3 ensures a bare % at the end of a string (not part of a valid %XX triplet) is still passed through rather than causing an index error.

Enabling and disabling shuffling per session

Shuffling is enabled by default when a session is created (shuffleDict is set to a random dictionary). You can toggle it via /editsession:
# disable shuffling for a session
curl "http://localhost:8080/editsession?id=SESSION_ID&enableShuffling=0"

# re-enable shuffling (generates a fresh dictionary)
curl "http://localhost:8080/editsession?id=SESSION_ID&enableShuffling=1"
The corresponding server-side logic in setupRoutes.js:
if (enableShuffling === '1' && !session.shuffleDict) {
    session.shuffleDict = StrShuffler.generateDictionary();
}
if (enableShuffling === '0') {
    session.shuffleDict = null;
}
When shuffleDict is null, Rammerhead skips the shuffle/unshuffle step and the destination URL is used as-is in proxy paths.
Shuffling is an obfuscation mechanism, not encryption. It hides URLs from casual inspection in logs and browser history, but it is not designed to withstand a determined attacker who has access to the session dictionary. For network-level privacy, run Rammerhead over HTTPS.
Without position-dependence, the mapping from source character to encoded character would be fixed across the entire URL. An attacker who saw many shuffled URLs from the same session could perform frequency analysis to recover the dictionary. Position-dependence ensures each character position has an independent substitution, defeating simple frequency attacks.
Extremely unlikely. Each session’s dictionary is an independent random permutation of 64 characters (64! ≈ 1.27 × 10^89 possible dictionaries). The probability of two sessions sharing an identical dictionary is negligible.

Sessions

How sessions store the shuffleDict and other per-user state

JavaScript caching

How Rammerhead caches rewritten JS to reduce CPU load

Build docs developers (and LLMs) love