While it works, I'd avoid the atom/ref approach, as these are not really the "Clojure way". (They are the correct tools for some problems, but not this one). The stack-frame approach is much more in line with Clojure philosophy.
To expand on the stack-frame solution, the application of each operation would be a function like:
new-stack (apply-op stack op)
Operations like + and - will do the obvious, while 'p' will have a side-effect (IO) but otherwise return the original stack.
The loop is then trivial:
(loop [stack [] op (get-op!)]
(if (not= 'q op)
(recur (apply-op stack op) (get-op!))))
I'm presuming a symbol of 'q' as the termination command, and get-op! to be the reading operation.
Also worth noting is that the "stack" is more naturally implemented with a list, as the required operations of first/rest/cons are already available. For instance, applying any binary operator to a list-based stack is just:
(cons (the-operator (first stack) (second stack)) (rest (rest stack)))
Or using destructuring for clarity:
(let [[a b & r] stack] (cons (the-operator a b) r))
Using a vector as a stack is not so easy, nor efficient.