As NickD points out, there is a hook that allows this. It falls under Advanced Configuration in the Org manual.
The export process executes two hooks before the actual exporting begins. The first hook,
org-export-before-processing-hook, runs before any expansions of macros, Babel code, and include keywords in the buffer. The second hook,
org-export-before-parsing-hook, runs before the buffer is parsed.
I think the
before-parsing hook is the better fit for this problem, but I’m not sure.
The manual doesn’t mention this, but the hook is actually an abnormal hook, so its functions must take an argument (the exporting backend).
It's also worth mentioning that these functions don't execute in the context of the org buffer you're exporting from. Like all export transformations, they run in a temporary buffer that starts with a copy of the org buffer's text.
To do the prefix insertion, use
org-babel-map-src-blocks and take advantage of how it dynamically binds useful context variables like the source block language and the beginning / end of the code body.
(defun my-insert-shell-prompt (_backend)
(org-babel-map-src-blocks nil ; nil implies current buffer
(let (;; capture macro-defined variables
;; other variables
(shell-langs '("sh" "shell"))
(prefix "$ "))
(when (member lang shell-langs)
(skip-chars-forward "\n\s-" end-body)
(while (< (point) end-body)
(skip-chars-forward "\n\s-" end-body)))))))
(add-hook 'org-export-before-parsing-hook #'my-insert-shell-prompt)