The MERGE command could be used on early games to load the first BASIC program block without it auto-running, but in later games, the use of MERGE caused the machine to hang. How was this achieved?
Each line of BASIC is stored in memory as two bytes for the line number, then two bytes indicating the length of the line in bytes, followed by the tokenised text of the line. MERGE protection works by poking the length bytes with bogus values that are larger than any valid program.
These bytes are ignored when running the program (at least, for simple programs that don't need to jump around between lines, which is usually the case for BASIC loaders of machine-code programs) but come into play when editing the program, or inserting lines - as MERGE does when used for its intended purpose. If the reported line length is larger than the available memory, this will fail with an out of memory error or a hard crash.
You can apply this technique to your own programs by saving them as follows (assuming that the BASIC program is in its standard location and hasn't been shifted around by the Spectrum ROM to accommodate microdrives or other peripherals):
SAVE "program" LINE 1
The most simple way to do this was saving the BASIC program not as a BASIC program but rather as raw bytes like in
100 PRINT "This appears before the SAVE!"
110 SAVE "DEMO" CODE 23552,1500
120 PRINT "The program goes on ...";
130 GO TO 120
The above code saves the currently running BASIC program together with the interpreter state and system variables as a binary snapshot - once it's put back into memory (with LOAD "DEMO" CODE), the program will continue to run where it was before saving. And because it's not saved as a BASIC program, there's no way to MERGE it. If you start saving from 16384, you can even restore the current screen contents together with your program. By increasing the byte count, you can even SAVE variable contents, loaded machine code, and/or defined UDGs that way, which would enable you to remove quite a bit of initialisation code and data from your BASIC program in order to save memory for code. To prevent a listing, you will need to revert to some BREAK inhibit POKEs to prevent the user from BREAKing into the program once it's loaded and continues to run.
Obviously, you could circumvent this by LOADing the image to a different (higher) address and examine the bytes from there (You'd need more sophisticated methods than a simple LIST, however, to do that). You can make this more difficult by simply making your program long enough so it won't fit at a higher address. But you might be running into difficulties supporting both the 16k and 48k Spectrum, then.
Another method for preventing MERGE, and which gives (slightly) less problems than modifying a BASIC line size in RAM, is changing the BASIC line number(s) with invalid numbers for the BASIC editor routines.
For instance, modifying the first line number (or several), for a 0, or any number line for a number greater than 9999.
So, you would enter a program as:
2 PRINT 234
After entering a program, to change the number of the first line to 0, in a standard 48K model, it is enough to change the first 2 bytes of the BASIC memory area, as they point to the beginning of the first BASIC line.
However, the BASIC starting memory location can change, for instance with an interface 1 present. The canon way is POKE (PROG),0 and POKE (PROG+1),0
LET B=PEEK(23635)+PEEK(23636)*256:POKE B,0:POKE B+1,0
After it, a LIST shows:
2 PRINT 234
Then it can be saved:
SAVE "program" LINE 2
As a side note, the line 0 trick is also useful to prevent an accidental edition of a BASIC line with a REM containing embedded machine code.
Nevertheless, protecting auto-run BASIC code based in crashing the MERGE routines has always been a very poor strategy. Back in the day before I got into serious assembly, I just saved and loaded a BASIC tape header of a much bigger BASIC program without auto-run and switched tapes after loading the header tape block. Presto, had already bypassed the anti-MERGE protection to have a quick look around, without much technical knowledge involved. For a more serious user, it is trivial to bypass, including saving an alternate header without auto-run, or nowadays editing the header in the TZX/TAP image file, or even patching the ROM to disable autorun.
For disabling LOAD "" autorun in a Spectrum 48K ROM, change ROM memory position 0x08A1 to 0x18 (replacing a JR NZ, with a JR).
08A1 JR NZ,LD_PROG_1 ; Jump if 'no number'; otherwise set NEWPPC and NSPPC.
08A1 JR LD_PROG_1