diff --git a/changelogs/unreleased/hide-show-prompt.md b/changelogs/unreleased/hide-show-prompt.md
new file mode 100644
index 0000000000000000000000000000000000000000..4b153dbf9674d3c2957b28288a461a5a7698f3fe
--- /dev/null
+++ b/changelogs/unreleased/hide-show-prompt.md
@@ -0,0 +1,4 @@
+## feature/lua/console
+
+* Prevent mixing of background print/log output with current user's input in
+  the interactive console (gh-7169).
diff --git a/src/box/lua/console.c b/src/box/lua/console.c
index 9fe0af12003d80e613d037641c621dd788cdd472..d66661b34862a20a678b8298915b79f91e8e8350 100644
--- a/src/box/lua/console.c
+++ b/src/box/lua/console.c
@@ -43,6 +43,7 @@
 #include "lua-yaml/lyaml.h"
 #include "main.h"
 #include "serialize_lua.h"
+#include "say.h"
 #include <lua.h>
 #include <lauxlib.h>
 #include <lualib.h>
@@ -50,6 +51,8 @@
 #include <readline/history.h>
 #include <stdlib.h>
 #include <ctype.h>
+#include <string.h>
+#include <strings.h>
 
 struct rlist on_console_eval = RLIST_HEAD_INITIALIZER(on_console_eval);
 
@@ -266,6 +269,192 @@ console_sigint_handler(ev_loop *loop, struct ev_signal *w, int revents)
 	fiber_wakeup(interactive_fb);
 }
 
+/* {{{ Show/hide prompt */
+
+/*
+ * The idea is borrowed from
+ * https://metacpan.org/dist/AnyEvent-ReadLine-Gnu/source/Gnu.pm
+ */
+
+static char *saved_prompt = NULL;
+static char *saved_line_buffer = NULL;
+static int saved_line_buffer_len = 0;
+static int saved_point = 0;
+
+static int console_hide_prompt_ref = LUA_NOREF;
+static int console_show_prompt_ref = LUA_NOREF;
+
+/**
+ * Don't attempt to hide/show prompt in certain readline states.
+ *
+ * There are readline states, where rl_message() is called
+ * internally. In this case an actual readline's line on the
+ * screen is not prompt + line buffer. Current code can't properly
+ * save and restore the line.
+ */
+static bool
+console_can_hide_show_prompt(void)
+{
+	if (RL_ISSTATE(RL_STATE_NSEARCH))
+		return false;
+	if (RL_ISSTATE(RL_STATE_ISEARCH))
+		return false;
+	if (RL_ISSTATE(RL_STATE_NUMERICARG))
+		return false;
+	return true;
+}
+
+/**
+ * Save and hide readline's output (prompt and current user
+ * input).
+ */
+static void
+console_hide_prompt(void)
+{
+	if (!console_can_hide_show_prompt())
+		return;
+
+	if (rl_prompt == NULL) {
+		saved_prompt = NULL;
+	} else {
+		saved_prompt = xstrdup(rl_prompt);
+	}
+	rl_set_prompt("");
+
+	saved_point = rl_point;
+
+	if (rl_line_buffer == NULL) {
+		saved_line_buffer = NULL;
+		saved_line_buffer_len = 0;
+	} else {
+		saved_line_buffer = xmalloc(rl_end + 1);
+		memcpy(saved_line_buffer, rl_line_buffer, rl_end);
+		saved_line_buffer[rl_end] = '\0';
+		saved_line_buffer_len = rl_end;
+	}
+	rl_replace_line("", 0);
+
+	rl_redisplay();
+}
+
+/**
+ * Show saved readline's output and free saved strings.
+ */
+static void
+console_show_prompt(void)
+{
+	if (!console_can_hide_show_prompt())
+		return;
+
+	rl_set_prompt(saved_prompt);
+	free(saved_prompt);
+	saved_prompt = NULL;
+
+	if (saved_line_buffer == NULL) {
+		rl_replace_line("", 0);
+	} else {
+		rl_replace_line(saved_line_buffer, saved_line_buffer_len);
+	}
+	free(saved_line_buffer);
+	saved_line_buffer = NULL;
+	saved_line_buffer_len = 0;
+
+	rl_point = saved_point;
+	saved_point = 0;
+
+	rl_redisplay();
+}
+
+static int
+lbox_console_hide_prompt(struct lua_State *L)
+{
+	(void)L;
+	console_hide_prompt();
+	return 0;
+}
+
+static int
+lbox_console_show_prompt(struct lua_State *L)
+{
+	(void)L;
+	console_show_prompt();
+	return 0;
+}
+
+/**
+ * Allow to disable hide/show prompt actions using an environment
+ * variable.
+ *
+ * It is not supposed to be a documented variable, but rather just
+ * a way to turn off the feature if something goes wrong.
+ */
+static bool
+console_hide_show_prompt_is_enabled(void)
+{
+	const char *envvar = getenv("TT_CONSOLE_HIDE_SHOW_PROMPT");
+
+	/* Enabled by default. */
+	if (envvar == NULL || *envvar == '\0')
+		return true;
+
+	/* Explicitly enabled or disabled. */
+	if (strcasecmp(envvar, "false") == 0)
+		return false;
+	if (strcasecmp(envvar, "true") == 0)
+		return true;
+
+	/* Accept 0/1 as boolean values. */
+	if (strcmp(envvar, "0") == 0)
+		return false;
+	if (strcmp(envvar, "1") == 0)
+		return true;
+
+	/* Can't parse the value, let's use the default. */
+	return true;
+}
+
+static void
+luaT_console_setup_write_cb(struct lua_State *L)
+{
+	if (!console_hide_show_prompt_is_enabled())
+		return;
+
+	say_set_stderr_callback(console_hide_prompt, console_show_prompt);
+
+	lua_getfield(L, LUA_GLOBALSINDEX, "package");
+	lua_getfield(L, -1, "loaded");
+	lua_getfield(L, -1, "internal.print");
+
+	lua_rawgeti(L, LUA_REGISTRYINDEX, console_hide_prompt_ref);
+	lua_setfield(L, -2, "before_cb");
+	lua_rawgeti(L, LUA_REGISTRYINDEX, console_show_prompt_ref);
+	lua_setfield(L, -2, "after_cb");
+
+	lua_pop(L, 3);
+}
+
+static void
+luaT_console_cleanup_write_cb(struct lua_State *L)
+{
+	if (!console_hide_show_prompt_is_enabled())
+		return;
+
+	say_set_stderr_callback(NULL, NULL);
+
+	lua_getfield(L, LUA_GLOBALSINDEX, "package");
+	lua_getfield(L, -1, "loaded");
+	lua_getfield(L, -1, "internal.print");
+
+	lua_pushnil(L);
+	lua_setfield(L, -2, "before_cb");
+	lua_pushnil(L);
+	lua_setfield(L, -2, "after_cb");
+
+	lua_pop(L, 3);
+}
+
+/* }}} Show/hide prompt */
+
 /* implements readline() Lua API */
 static int
 lbox_console_readline(struct lua_State *L)
@@ -302,6 +491,8 @@ lbox_console_readline(struct lua_State *L)
 	if (readline_L != NULL)
 		luaL_error(L, "readline(): earlier call didn't complete yet");
 
+	luaT_console_setup_write_cb(L);
+
 	readline_L = L;
 
 	if (completion) {
@@ -344,6 +535,8 @@ lbox_console_readline(struct lua_State *L)
 				lua_pushstring(L, "");
 				lua_pushboolean(L, sigint_called);
 
+				luaT_console_cleanup_write_cb(L);
+
 				readline_L = NULL;
 				sigint_called = false;
 				set_sigint_cb(old_cb);
@@ -355,8 +548,10 @@ lbox_console_readline(struct lua_State *L)
 			 * we might spin here forever eating
 			 * the whole cpu time.
 			 */
-			if (fiber_is_cancelled())
+			if (fiber_is_cancelled()) {
+				luaT_console_cleanup_write_cb(L);
 				set_sigint_cb(old_cb);
+			}
 			luaL_testcancel(L);
 		}
 		rl_callback_read_char();
@@ -366,6 +561,7 @@ lbox_console_readline(struct lua_State *L)
 	/* Incidents happen. */
 #pragma GCC poison readline_L
 	rl_attempted_completion_function = NULL;
+	luaT_console_cleanup_write_cb(L);
 	set_sigint_cb(old_cb);
 	luaL_testcancel(L);
 	return 2;
@@ -744,6 +940,11 @@ tarantool_lua_console_init(struct lua_State *L)
 	};
 	session_vtab_registry[SESSION_TYPE_CONSOLE] = console_session_vtab;
 	session_vtab_registry[SESSION_TYPE_REPL] = console_session_vtab;
+
+	lua_pushcfunction(L, lbox_console_hide_prompt);
+	console_hide_prompt_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+	lua_pushcfunction(L, lbox_console_show_prompt);
+	console_show_prompt_ref = luaL_ref(L, LUA_REGISTRYINDEX);
 }
 
 /*
diff --git a/test/box-luatest/hide_show_prompt_test.lua b/test/box-luatest/hide_show_prompt_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..eed4d9da1a82e0e586a2997607a132ae8fa60b5b
--- /dev/null
+++ b/test/box-luatest/hide_show_prompt_test.lua
@@ -0,0 +1,57 @@
+local it = require('test.luatest_helpers.interactive_tarantool')
+
+local t = require('luatest')
+local g = t.group()
+
+-- Basic case: prompt is shown and hid.
+--
+-- It does not check that tarantool preserves current input line
+-- and cursor position.
+g.test_basic_print = function()
+    local child = it.new({args = {'-l', 'fiber'}})
+
+    child:execute_command([[
+        _ = fiber.create(function()
+            fiber.name('print_flood', {truncate = true})
+            while true do
+                print('flood')
+                fiber.sleep(0.001)
+            end
+        end)
+    ]])
+    child:assert_empty_response()
+
+    local exp_line = it.PROMPT .. it.CR .. it.ERASE_IN_LINE .. 'flood'
+    for _ = 1, 10 do
+        child:assert_line(exp_line)
+    end
+
+    child:close()
+end
+
+-- The same as the basic case, but prints flood using logger to
+-- stderr.
+--
+-- We don't check for presence of 'flood' lines in the log, but
+-- verify that prompt on stdout is shown and hid.
+g.test_basic_log = function()
+    local child = it.new({args = {'-l', 'fiber', '-l', 'log'}})
+
+    child:execute_command([[
+        _ = fiber.create(function()
+            fiber.name('log_flood', {truncate = true})
+            while true do
+                log.info('flood')
+                fiber.sleep(0.001)
+            end
+        end)
+    ]])
+    child:assert_empty_response()
+
+    local exp_data = it.PROMPT .. it.CR .. it.ERASE_IN_LINE
+    for _ = 1, 10 do
+        child:assert_data(exp_data)
+    end
+
+    child:close()
+end