I think I have found the genesis of these playback ep bugs in Atomic.
An analysis of
github.com/ornicar/chess.js/blob/master/chess.jsNote the make_move() and undo_move() are being called constantly in the replayer, to check for legality and SAN for instance, so any mishap in them will show up.
Consider a case of dxc6 en passant.
At line 833, c6 is set to a wP and d5 is cleared.
At line 838-839, c5 is cleared.
Now at 854 there are explosions.
The first, set at line 429, is a black (sic) pawn at c6, normally this is the explosion of the capturer, and the stored information (color/type) is the captured piece. In the ep case, move.to and captures_on differ. The second explosion, set at line 445, is a black pawn at c5, which I think is redundant, but can't hurt.
Now over to the undo, where at line 952 the explosions are undone, first there is *incorrectly* set a black pawn at c6, and then (correctly) a black pawn at c5. This explains why these ghost pawns appear when the ep capture is not made, in happens always in Atomic replaying. At line 955, d5 is set to a wP, and then at line 973 c5 is (redunantly, but correctly) set to a bP.
So to fix this, I think one only need to remove the extra pawn that appeared from this process in the ep case, say as part of the line 966 conditional.
board[move.to]=null;
A simple test case:
[White "me"]
[Black "you"]
[Variant "atomic"]
[Result "*"]
1. d4 Nf6 2. d5 c5 3. h4 *
Currently playing h4 on the replay board makes a bP at c6 appear, and hopefully my one-line fix will remedy this.