StegoToolkit

Invisible Text Hider

Hide secret messages inside visible text using zero-width Unicode characters. Add optional AES-256-GCM encryption and check payload capacity before encoding. Processing happens in your browser; the text and payload are not uploaded to StegoToolkit's server.

Paste Your Cover Text

This is the visible text your recipient will see. Your secret message will be invisibly embedded inside it.

Cover text length matters. Longer cover text = more hiding capacity. Use a natural-looking excerpt — news articles, Wikipedia paragraphs, or email text work well.

Try the demo instantly

Loads a sample cover text — no typing needed

Longer text = more capacity

Processed in your browser

Your cover text and secret message are encoded in this browser tab and are not sent to StegoToolkit's server. Usage statistics may record which options were used, never your text.

How to Hide a Message in Plain Text (4 steps)

  1. Paste or upload the visible cover text — the words people will actually see. A longer text holds a longer message.
  2. Type the secret message (or add a file) and, if you want it protected, a password for AES-256-GCM encryption.
  3. Choose an encoding mode. Binary is the default; Quaternary and Octal hold more per character but need a platform that lets all their characters through.
  4. Click Encode, copy the result and paste it where you want it. Run it through the Invisible Text Extractor first to confirm it survives that app.

Zero-Width Steganography Methods — What This Tool Handles

MethodTechniqueCompatibilityBest For
Binary (ZWNJ / ZWJ)1 bit per inserted character: U+200C = 0, U+200D = 1121 bytes in a 1,000-character cover textDefault — the smallest character set, the one most CTF puzzles use
Quaternary (4 characters)2 bits per character: ZWNJ, ZWJ, U+2060, U+200E246 bytes in 1,000 charactersMore capacity from a short cover text
Octal (8 characters)3 bits per character, 8 different zero-width characters371 bytes in 1,000 characters (only with the Plain Text File platform)Maximum capacity; every other platform setting falls back to Binary
AES-256-GCM encryptionPBKDF2 (100,000 iterations) with a random salt and IV48 bytes of encryption overheadThe payload is encrypted before it is turned into characters
Reed-Solomon (RS-10/20/30)Parity bytes added to the payload (10, 20 or 30 per 223-byte block, plus a 16-byte header)26 bytes of overhead for a 100-byte payload at RS-10Repairs altered bytes within its limit, cannot restore removed characters — usually leave it off

How a Message Becomes Invisible Characters

The message (and, if you set one, its AES-256-GCM encryption) is turned into bytes, a 4-byte length is put in front, and the bytes are written as a stream of zero-width characters: in Binary mode one character per bit (U+200C for 0, U+200D for 1), in Quaternary two bits per character, in Octal three. Those characters are then inserted after characters of your cover text. They have no width, so the text looks the same, but they are still characters: the text is longer, and a program that lists code points sees them immediately. Encryption protects the content of the message; it does not hide that something is hidden.

Capacity figures are the largest message sizes for a 1,000-character cover assuming no password, no file name and no Reed-Solomon; encryption, file metadata and error-correction data reduce the room, and the capacity bar accounts for your settings. Capacity follows from that: one insertion point per visible character (an emoji counts once). In a 1,000-character cover text Binary holds 121 bytes of message, Quaternary 246 and Octal 371; each figure is already net of the 4-byte length, for a typed message with no Reed-Solomon and no file name (a hidden file's name and any Reed-Solomon parity are subtracted as well). With a password the encryption wrapper adds 48 bytes to the stored data, so the message you can send is 48 bytes smaller (Binary: 73 bytes). The capacity bar uses the same function as the encoder, so a message that the bar accepts is a message the encoder accepts.

Platform Setting: What It Does and Does Not Do

Platform settings control which zero-width characters each mode may use. Quaternary needs four characters and Octal eight, so a mode falls back to Binary when the selected platform's list does not contain all of them. The lists in this tool: Plain Text File allows all nine; Discord allows U+200C, U+200D, U+2060, U+200E and U+200F; Gmail allows U+200C, U+200D, U+2060 and U+200E; Slack allows U+200C, U+200D and U+2060; Twitter / X and WhatsApp allow only U+200C and U+200D; General (destination unknown) allows only U+200C and U+200D when the mode is chosen, but Binary mode under General may use any of its four pairs. Binary normally writes with U+200C/U+200D. If your cover text already contains one of those, the tool uses another pair that the platform allows and that the cover does not contain: the four pairs are U+200C/U+200D, U+200B/U+200D, U+200C/U+2060 and U+200E/U+200F, which gives Plain Text File and General four, Discord three, Gmail and Slack two, and Twitter / X and WhatsApp one. If no allowed pair is free of the cover's characters, the cover's conflicting characters are removed and the result says how many. If the chosen platform does not allow every character of the chosen mode, the text is written in Binary mode instead, and the capacity shown drops to match: Octal on Discord holds 121 bytes in 1,000 characters, the same as Binary, not 371.

We have not tested what Gmail, Discord, X, Slack, WhatsApp or any other service does to these characters, and we do not publish a survival table. Some applications remove or replace invisible characters when text is pasted, edited or posted, and the behaviour can change without notice. Before you rely on a message, paste the encoded text into the real destination, copy it back out and decode it with the Invisible Text Extractor.

Choosing a Cover Text

Use text that reads naturally and is long enough: a short sentence holds only a few bytes. Invisible characters are inserted only after whole visible characters — an emoji, a family or flag sequence, or a letter with its combining marks counts once and is never split.

If the cover text already contains some of the invisible characters this tool writes (for example U+200C/U+200D in Persian text or inside emoji sequences, or direction marks in Arabic or Hebrew), the cover's own characters are not deleted. When the cover contains a character of the usual ZWNJ/ZWJ pair, the tool looks for another Binary pair that the chosen platform allows and the cover does not use (ZWNJ/WJ, ZWSP/ZWJ or LRM/RLM), writes your message with it and tells you so; the left-to-right / right-to-left marks can change display order in mixed-script text. Which pairs are on offer depends on the platform: General and Plain Text File may use any of the four, Discord can use LRM/RLM, Gmail and Slack allow only ZWNJ/ZWJ and ZWNJ/WJ, and Twitter / X and WhatsApp only ZWNJ/ZWJ, so a cover that contains ZWNJ has no free pair on Gmail, Slack, Twitter / X or WhatsApp. When no allowed pair is free, the cover's own copies of the characters in use are removed and counted so the Extractor cannot read them as message bits — removing ZWJ splits emoji sequences and removing ZWNJ changes how Persian, Arabic and Indic letters join. In our run on a Persian, family-emoji and flag cover (60 cover characters), General, Plain Text File and Discord left all 60 in place, while Gmail, Slack, Twitter / X and WhatsApp removed them. Use General or Plain Text File to keep the cover untouched. Invisible characters outside the set in use (U+FEFF, for instance) are left alone. Inserted characters can still change how some scripts join letters, so check the result in the place where it will be used.

Insertion positions: Random-seed spreads the characters over the text using a seeded shuffle, After-word puts them after spaces, Every-N puts one after every Nth character and Start puts them all at the beginning. The position pattern changes how the text looks to a code-point viewer; it does not change what the Extractor needs — decoding does not require the seed.

What Hiding Here Does Not Protect Against

This is concealment, not secrecy. Anyone who runs the text through a tool that shows zero-width characters (including the Extractor on this site) will see that something is there, how many characters there are and which types. Encryption protects the payload's contents, but it does not conceal the presence of the zero-width characters: a character-inspection tool can reveal the hiding technique and may expose that an encoded payload exists, and Binary mode with a password produces a sequence of zero-width characters that is detectable even though the text looks unchanged. Do not use it to protect anything whose discovery would be dangerous.

Examples

Input: Cover: a 2,700-character paragraph. Message: 'Meet at noon' (12 bytes). Binary mode, no password.

Result: 128 zero-width characters are inserted (12 bytes + the 4-byte length = 16 bytes = 128 bits).

Meaning: The text looks unchanged; the Extractor's Auto-Detect returns 'Meet at noon'.

Input: The same message with the password 'pw-1'.

Result: 512 characters are inserted (12 + 48 bytes of encryption wrapper + 4 = 64 bytes = 512 bits).

Meaning: Encryption makes the hidden part four times longer for a short message.

Input: Cover: 1,000 characters, Octal mode, platform Discord.

Result: Written in Binary mode, capacity 121 bytes (Octal with 'Plain Text File' would hold 371).

Meaning: The platform setting overrides the mode when it cannot carry all eight characters.

Input: A file named pic.png (48 bytes) hidden in a 10,000-character cover, Binary mode, no password.

Result: 488 zero-width characters are inserted: 2 bytes + the 7-byte name + 48 bytes + the 4-byte length = 61 bytes = 488 bits.

Meaning: The Extractor shows the name pic.png and the type PNG, and Download saves the file under its own name. A typed message carries no name.

Input: A message one byte over the capacity bar.

Result: The encoder refuses with 'Cover text too short' and says how many characters to add.

Meaning: The limit is exact: the largest message the bar shows round-trips; one more byte is refused.

How the Invisible Text Hider Works

1. Check the cover text

The character pair is chosen so it does not collide with invisible characters already in the cover (a different pair is used when needed); any remaining collisions are removed and counted.

2. Encrypt and protect (optional)

With a password the payload is wrapped with AES-256-GCM (PBKDF2, 100,000 iterations, random salt and IV). Reed-Solomon parity can be added after encryption.

3. Frame and convert

A 4-byte big-endian length is put in front and the bytes are written as bits, 2-bit or 3-bit groups of zero-width characters.

4. Insert

Characters are inserted after whole characters of the cover text — never inside an emoji, emoji sequence, surrogate pair or letter-plus-mark cluster — at positions given by the scatter setting. Everything runs in your browser tab.

What the results mean

ResultWhat it meansWhat to do
Message Hidden SuccessfullyThe encoded text was produced and its zero-width characters counted.Copy it, then test it in the destination with the Extractor.
Cover text too shortThe message (plus encryption and parity overhead) needs more insertion points than the cover text has.Add cover text, shorten the message, or use Quaternary (or Octal with the Plain Text File platform).
Nothing to hideThe message is empty (no text and no file).Type a message or choose a file; an empty payload is refused instead of producing a file that cannot be decoded.
Characters removed from the cover textThe cover text held characters of the pair in use and no other pair was available for the platform; they were removed first.Review the output before using it. The tool removed the cover's conflicting characters because no allowed pair was free; this can affect emoji sequences or text shaping in some scripts. The Extractor then reads only your message.
A different character pair was usedThe cover text already contained ZWNJ/ZWJ (emoji sequences, Persian text), so the tool selected another Binary pair and the cover's characters were left untouched.Nothing to do: Auto-Detect in the Extractor tries all four Binary pairs this tool can use.

Limitations

  • No platform has been tested. The platform setting limits which characters are used; it is not a promise that the text will survive there.
  • Zero-width characters are visible to any tool that lists code points, and a text-diff or a 'show invisible characters' view shows them at once.
  • Reed-Solomon repairs altered bytes but cannot restore characters that an application removed; removing a character shifts every bit after it.
  • Plausible deniability is not offered here. For a two-layer decoy mode use the Text Steganography Hider.
  • Characters other than the nine this tool recognises (for example U+FEFF or U+180E) are neither written nor removed.
  • The 4-byte length at the start of the message is not covered by Reed-Solomon: if it is altered, the message cannot be read even though the rest could be repaired.
  • Per-recipient copies need a cover text long enough for each copy. Each copy hides only a short recipient tag, 20 bytes plus the recipient's name in UTF-8 and the 48-byte encryption wrapper, and not your secret message. A copy that does not fit is left out of the ZIP and listed instead of being delivered empty. The tag is attribution, not copy protection: it cannot stop copying or editing, and removing the zero-width characters removes it.

Privacy

Your cover text, message, file and password are processed in this browser tab and are not sent to StegoToolkit's server. Usage statistics may record which options were used (mode, platform, scatter type) and step timings, never your text; Google Analytics and Ahrefs load only after you accept, and Vercel's cookieless page-view counter always runs. Your host's request log sees the page request as for any website.

Which Tool Next?

Frequently Asked Questions

Can anyone see the hidden message just by looking?

Not by reading: zero-width characters have no width, so the text looks the same. But they are real characters. Selecting the text, counting its length, a code-point viewer or the Invisible Text Extractor reveals them. With a password the content is unreadable without it, but the presence of a payload is not hidden.

Does the hidden message survive copy and paste?

The characters are part of the text, so ordinary copy and paste normally keeps them. Whether a particular website or app keeps them is something we have not tested, and some strip or replace invisible characters. Test in the real destination before relying on it.

Which encoding mode should I use?

Binary unless you need room. It uses two characters, holds 121 bytes per 1,000 characters of cover text, and is what most CTF puzzles use. Quaternary holds 246 and Octal 371, but the platform setting must allow all their characters; otherwise the tool writes Binary and the capacity bar shows the smaller number.

What is the seed? Do I need it to decode?

The seed picks the insertion positions for Random-seed scatter, so the same cover text and seed produce the same output. The Extractor does not need it: it reads every zero-width character in order, wherever it sits. The seed is not a password and adds no security.

Does it work in Gmail, Discord or X?

We do not know, because we have not tested them. Selecting a platform restricts the character set (Twitter / X, WhatsApp and General use only two characters); it does not guarantee delivery. Paste into the real app, copy it back and decode it before you rely on it.

Can I hide a file instead of text?

Yes: switch the payload to a file. Any file is stored as raw bytes, limited by the capacity bar. A file of a few kilobytes needs a cover text of tens of thousands of characters, so this is practical only for small files.

What does Reed-Solomon do here?

RS-10/20/30 adds parity (10, 20 or 30 bytes per 223-byte block plus a 16-byte header; 26 bytes extra for a 100-byte payload at RS-10) and repairs altered bytes within its correction limits. It cannot restore zero-width characters that an application removed: deleting a character shifts the rest of the encoded bit stream, which disrupts decoding. The 4-byte length at the start is outside the Reed-Solomon protection, so damage to it can prevent recovery even when the rest could be repaired. It therefore does not make a message survive stripping. Leave it off unless you expect bytes to be altered.

Does hiding a file keep its name?

Yes. The name travels inside the payload, under the same password and Reed-Solomon protection, and the Extractor shows it with the recognised file type. It costs 2 bytes plus the name's length in UTF-8, and the capacity bar counts it; names in any script work (a file named фото-写真.png came back under that name), and a name longer than 255 UTF-8 bytes is cut at 255. A typed message has no name.

Can I make one copy per recipient to find a leak?

Batch Watermarking creates separate, recipient-tagged copies of the encoded text; it supports leak attribution, not copy prevention. Yes, in four steps. 1) Encode once as usual: Batch Watermarking is a panel on the result screen, and it reuses that encode's cover text and settings (mode, platform, scatter, seed, Reed-Solomon), not your secret message. 2) Open Batch Watermarking, enter up to 200 recipients (one per line; duplicate lines are ignored and counted) and a master secret. 3) Click Generate & Download ZIP: each copy hides the tag WM:<ID>:<recipient>, where the ID is the first 16 hexadecimal characters (64 bits) of an HMAC-SHA-256 of the recipient name under the master secret, and the tag is encrypted with the master secret as the password. The ZIP (watermarked-texts.zip) holds one .txt per recipient, with file names made unique even for recipients that look alike (characters other than A-Z, a-z, 0-9, . _ and - become _; clashes get a number), plus watermark-key.json mapping each recipient to the ID. A copy that cannot be made (cover text too short) is left out and listed under _notCreated in the key file and on screen. 4) To identify a leaked copy, decode it in the Extractor with the master secret as the password, open Watermark Attribution on the result and paste the contents of watermark-key.json. Keep the key file and the master secret safe: without them a copy cannot be traced to a recipient. This is attribution only; it does not stop copying, and anyone who strips the zero-width characters (or retypes the text) removes the tag.

Is my text uploaded anywhere?

No. Encoding and AES-GCM encryption run in your browser tab with the Web Crypto API, and in our browser check no request carried the text, message, file or password. Usage statistics may record the options you used and step timings, never your text or password; Google Analytics and Ahrefs load only after you accept, while Vercel's cookieless page-view counter always runs, and the host keeps ordinary request logs.

Related Tools