I can think of one (ugly) way to do it:
Attributes[myHold] = {HoldAll};
expr /.
f[args__] :> With[
{res = myHold[args] /. Print -> Echo},
f @@ res /; True
] /.
HoldPattern[f_ @@ myHold[args__]] :> f[args]
(* Hold[{f[{1, {Echo[1]}}], g[{{{Print[1]}}}]}] *)
The idea is to wrap the contents of f
inside a function with HoldAll
attribute (not Hold
, to be able to identify it uniquely later on). In a first step, the expression is returned with myHold[…]
still in place. In a second round of replacements, myHold
is stripped out again.