The tokcycle package processes its input token-by-token according to user-defined directives. It has environment forms and macro forms. In either form, # tokens are captured as cat-6, a \catSIXtrue flag is set, but they are momentarily converted to cat-12 to be processed by the Character directive.
Here, I will just tell the character directive to output the (now cat-12 #) token it gets, regardless of whether \catSIXtrue or not (this is actually the default).
I present three approaches using this technique. In the first, just apply the \message command as an argument to the \tokcyclexpress macro. Then, regurgitate the token list \the\cytoks.
In the second environment approach, wrap the \message{...} command in \tokencyclexpress...\endtokencyclexpress.
Finally, in the third approach, I define a new tokcycle environment \msg...\endmsg, where you just insert what would otherwise be the \message argument.
Because the processed # tokens in the token cycle are now cat-12, they are not doubled.
\documentclass{article}
\usepackage{tokcycle}[2021/03/10]
\begin{document}
Direct Macro Method:
\tokcyclexpress{\message{#1#\relax#2}}
\the\cytoks
Direct Environment Method:
\tokencyclexpress\message{#1#\relax#2}\endtokencyclexpress
Special environment method:
\xtokcycleenvironment\msg
{\addcytoks{##1}}
{\processtoks{##1}}
{\addcytoks{##1}}
{\addcytoks{##1}}
{}
{\cytoks\expandafter{\expandafter\message\expandafter{\the\cytoks}}}
\msg #1#\relax#2\endmsg
Now go check the log file.
\end{document}
Screen capture of log file:

\stringyields a single character token while "hitting" them with\detokenizeyields two character tokens. You may need to crank out the case of catcode-6-spaces because stringifying/detokenizing them will yield space-tokens of catcode 10 which cannot be grabbed as non-delimited arguments... – Ulrich Diez Dec 14 '19 at 14:16\futurelettogether with the fact that spaces are not skipped after single character control sequences (see also the implementation of\@ifnextcharin LaTeX). – Henri Menke Dec 15 '19 at 06:31\StringifyNActfor stringifying each token of a macro argument. That loop also makes use of a macro\UD@CheckWhetherLeadingSpace. Based on that one can derive something which does stringify explicit catcode1/2-characters and explicit hashes only. The result can be nested into\message{\unexpanded{}}... – Ulrich Diez Dec 15 '19 at 10:06