diff --git a/docs/docs/usage/scripts.md b/docs/docs/usage/scripts.md
index 483d7c1b..12c97c66 100644
--- a/docs/docs/usage/scripts.md
+++ b/docs/docs/usage/scripts.md
@@ -108,6 +108,8 @@ all = {composite = ["lint", "test"]}
 
 Running `pdm run all` will run `lint` first and then `test` if `lint` succeeded.
 
++++ 2.13.0
+
 To override the default behavior and continue the execution of the remaining
 scripts after a failure, set the `keep_going` option to `true`:
 
@@ -179,6 +181,20 @@ start.env_file.override = ".env"
 !!! note
     A dotenv file specified on a composite task level will override those defined by called tasks.
 
+### `working_dir`
+
++++ 2.13.0
+
+You can set the current working directory for the script:
+
+```toml
+[tool.pdm.scripts]
+start.cmd = "flask run -p 54321"
+start.working_dir = "subdir"
+```
+
+Relative paths are resolved against the project root.
+
 ### `site_packages`
 
 To make sure the running environment is properly isolated from the outer Python interpreter,
diff --git a/news/2620.feature.md b/news/2620.feature.md
new file mode 100644
index 00000000..e3ad4ffb
--- /dev/null
+++ b/news/2620.feature.md
@@ -0,0 +1,1 @@
+Add an option `working_dir` for PDM's scripts to set the current working directory.
diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py
index 36f7ed64..506072eb 100644
--- a/src/pdm/cli/commands/run.py
+++ b/src/pdm/cli/commands/run.py
@@ -36,11 +36,12 @@ if TYPE_CHECKING:
         help: str
         keep_going: bool
         site_packages: bool
+        working_dir: str
 
 
 def exec_opts(*options: TaskOptions | None) -> dict[str, Any]:
     return dict(
-        env={k: v for opts in options if opts for k, v in opts.get("env", {}).items()},
+        env={k: v for opts in options if opts for k, v in opts.get("env", {}).items()} or None,
         **{k: v for opts in options if opts for k, v in opts.items() if k not in ("env", "help")},
     )
 
@@ -104,7 +105,7 @@ class TaskRunner:
     """The task runner for pdm project"""
 
     TYPES = ("cmd", "shell", "call", "composite")
-    OPTIONS = ("env", "env_file", "help", "keep_going", "site_packages")
+    OPTIONS = ("env", "env_file", "help", "keep_going", "working_dir", "site_packages")
 
     def __init__(self, project: Project, hooks: HookManager) -> None:
         self.project = project
@@ -159,6 +160,7 @@ class TaskRunner:
         site_packages: bool = False,
         env: Mapping[str, str] | None = None,
         env_file: EnvFileOptions | str | None = None,
+        working_dir: str | None = None,
     ) -> int:
         """Run command in a subprocess and return the exit code."""
         project = self.project
@@ -213,7 +215,7 @@ class TaskRunner:
                 # Don't load system site-packages
                 process_env["NO_SITE_PACKAGES"] = "1"
 
-        cwd = project.root if chdir else None
+        cwd = (project.root / working_dir) if working_dir else project.root if chdir else None
 
         def forward_signal(signum: int, frame: FrameType | None) -> None:
             if sys.platform == "win32" and signum == signal.SIGINT:
@@ -285,12 +287,7 @@ class TaskRunner:
                         return code
                     composite_code = code
             return composite_code
-        return self._run_process(
-            args,
-            chdir=True,
-            shell=shell,
-            **exec_opts(self.global_options, options, opts),
-        )
+        return self._run_process(args, chdir=True, shell=shell, **exec_opts(self.global_options, options, opts))
 
     def run(self, command: str, args: list[str], opts: TaskOptions | None = None, chdir: bool = False) -> int:
         if command in self.hooks.skip:
@@ -312,11 +309,7 @@ class TaskRunner:
             self.hooks.try_emit("post_script", script=command, args=args)
             return code
         else:
-            return self._run_process(
-                [command, *args],
-                chdir=chdir,
-                **exec_opts(self.global_options, opts),
-            )
+            return self._run_process([command, *args], chdir=chdir, **exec_opts(self.global_options, opts))
 
     def show_list(self) -> None:
         if not self.project.scripts:
diff --git a/src/pdm/termui.py b/src/pdm/termui.py
index 22eee13c..2984571e 100644
--- a/src/pdm/termui.py
+++ b/src/pdm/termui.py
@@ -8,6 +8,7 @@ import tempfile
 import warnings
 from typing import TYPE_CHECKING
 
+import rich
 from rich.box import ROUNDED
 from rich.console import Console
 from rich.progress import Progress, ProgressColumn
@@ -36,21 +37,21 @@ DEFAULT_THEME = {
     "info": "blue",
     "req": "bold green",
 }
-_console = Console(highlight=False, theme=Theme(DEFAULT_THEME))
+rich.reconfigure(highlight=False, theme=Theme(DEFAULT_THEME))
 _err_console = Console(stderr=True, theme=Theme(DEFAULT_THEME))
 
 
 def is_interactive(console: Console | None = None) -> bool:
     """Check if the terminal is run under interactive mode"""
     if console is None:
-        console = _console
+        console = rich.get_console()
     return console.is_interactive
 
 
 def is_legacy_windows(console: Console | None = None) -> bool:
     """Legacy Windows renderer may have problem rendering emojis"""
     if console is None:
-        console = _console
+        console = rich.get_console()
     return console.legacy_windows
 
 
@@ -61,6 +62,7 @@ def style(text: str, *args: str, style: str | None = None, **kwargs: Any) -> str
     :param style: rich style to apply to whole string
     :return: string containing ansi codes
     """
+    _console = rich.get_console()
     if _console.legacy_windows or not _console.is_terminal:  # pragma: no cover
         return text
     with _console.capture() as capture:
@@ -176,7 +178,7 @@ class UI:
 
         :param theme: dict of theme
         """
-        _console.push_theme(theme)
+        rich.get_console().push_theme(theme)
         _err_console.push_theme(theme)
 
     def echo(
@@ -193,7 +195,7 @@ class UI:
         :param verbosity: verbosity level, defaults to QUIET.
         """
         if self.verbosity >= verbosity:
-            console = _err_console if err else _console
+            console = _err_console if err else rich.get_console()
             if not console.is_interactive:
                 kwargs.setdefault("crop", False)
                 kwargs.setdefault("overflow", "ignore")
@@ -223,7 +225,7 @@ class UI:
         for row in rows:
             table.add_row(*row)
 
-        _console.print(table)
+        rich.print(table)
 
     @contextlib.contextmanager
     def logging(self, type_: str = "install") -> Iterator[logging.Logger]:
@@ -276,12 +278,7 @@ class UI:
 
     def make_progress(self, *columns: str | ProgressColumn, **kwargs: Any) -> Progress:
         """create a progress instance for indented spinners"""
-        return Progress(
-            *columns,
-            console=_console,
-            disable=self.verbosity >= Verbosity.DETAIL,
-            **kwargs,
-        )
+        return Progress(*columns, disable=self.verbosity >= Verbosity.DETAIL, **kwargs)
 
     def info(self, message: str, verbosity: Verbosity = Verbosity.QUIET) -> None:
         """Print a message to stdout."""

diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
index e2d4c4d9..6e1e4c03 100644
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -900,3 +900,15 @@ def test_empty_positional_args_display_help(project, pdm):
     assert "Usage:" in result.output
     assert "Commands:" in result.output
     assert "Options:" in result.output
+
+
+def test_run_script_changing_working_dir(project, pdm, capfd):
+    project.root.joinpath("subdir").mkdir()
+    project.root.joinpath("subdir", "file.text").write_text("Hello world\n")
+    project.pyproject.settings["scripts"] = {
+        "test_script": {"working_dir": "subdir", "cmd": "cat file.text"},
+    }
+    project.pyproject.write()
+    capfd.readouterr()
+    pdm(["run", "test_script"], obj=project, strict=True)
+    assert capfd.readouterr()[0].strip() == "Hello world"

Add an option for PDM's scripts to set the current working directory
## Is your feature/enhancement proposal related to a problem? Please describe.

This issue suggest adding a new key for commands defined under `tool.pdm.scripts`, to set the current working directory before running the given action.

Consider the following project structure:

```
my_project/
├─ docs/
│  ├─ source/
│  ├─ Makefile
│  ├─ make.bat
├─ src/
│  ├─ my_package/
├─ pyproject.toml
```
This is a recurring structure given Sphinx's proposed setup.
There are commands that can only be ran in the `docs/` directory, due to the nature of `make` that uses the `Makefile` available at the root of the current working directory.

## Describe the solution you'd like

This key, which could be called `cwd`, or `in_path`, or any given suggestion, would provide an OS-agnostic way to set the current working directory.

When a command will be running, and this property is set with a relative path (In this example `./docs`), PDM should come back to the root of the project (`pdm info --where`), so that moving in directories is seamless, and would not surprise us in case our current working directory was in `src/my_package`, for example.

Alternatively, this could be done by using operators like `&&` in a shell script, but such operator (In my knowledge) could be different depending on the OS used.

Adding such key would, in my opinion, increase confidence when creating new scripts that require to move to specific directories.

- - -

I am willing to work on this issue and submit a PR.
tests/cli/test_run.py::test_run_script_changing_working_dir
tests/cli/test_run.py::test_auto_isolate_site_packages tests/cli/test_run.py::test_run_with_site_packages tests/cli/test_run.py::test_run_command_not_found tests/cli/test_run.py::test_run_pass_exit_code tests/cli/test_run.py::test_run_cmd_script tests/cli/test_run.py::test_run_cmd_script_with_array tests/cli/test_run.py::test_run_script_pass_project_root tests/cli/test_run.py::test_run_shell_script tests/cli/test_run.py::test_run_script_with_relative_path tests/cli/test_run.py::test_run_non_existing_local_script tests/cli/test_run.py::test_run_shell_script_with_args_placeholder[with-args] tests/cli/test_run.py::test_run_shell_script_with_args_placeholder[without-args] tests/cli/test_run.py::test_run_shell_script_with_args_placeholder_with_default[with-args] tests/cli/test_run.py::test_run_shell_script_with_args_placeholder_with_default[with-default] tests/cli/test_run.py::test_run_call_script tests/cli/test_run.py::test_run_script_with_extra_args tests/cli/test_run.py::test_run_script_with_args_placeholder[as-str-with-args] tests/cli/test_run.py::test_run_script_with_args_placeholder[as-str-without-args] tests/cli/test_run.py::test_run_script_with_args_placeholder[as-list-with-args] tests/cli/test_run.py::test_run_script_with_args_placeholder[as-list-without-args] tests/cli/test_run.py::test_run_script_with_args_placeholder_with_default[as-str-with-args] tests/cli/test_run.py::test_run_script_with_args_placeholder_with_default[as-str-default] tests/cli/test_run.py::test_run_script_with_args_placeholder_with_default[as-list-with-args] tests/cli/test_run.py::test_run_script_with_args_placeholder_with_default[as-list-default] tests/cli/test_run.py::test_run_shell_script_with_pdm_placeholder tests/cli/test_run.py::test_run_expand_env_vars tests/cli/test_run.py::test_run_expand_env_vars_from_config tests/cli/test_run.py::test_run_script_with_env_defined tests/cli/test_run.py::test_run_script_with_dotenv_file tests/cli/test_run.py::test_run_script_override_global_env tests/cli/test_run.py::test_run_show_list_of_scripts tests/cli/test_run.py::test_run_show_list_of_scripts_hide_internals tests/cli/test_run.py::test_run_json_list_of_scripts tests/cli/test_run.py::test_import_another_sitecustomize tests/cli/test_run.py::test_run_with_patched_sysconfig tests/cli/test_run.py::test_run_composite tests/cli/test_run.py::test_composite_stops_on_first_failure tests/cli/test_run.py::test_composite_keep_going_on_failure tests/cli/test_run.py::test_composite_inherit_env tests/cli/test_run.py::test_composite_fail_on_first_missing_task tests/cli/test_run.py::test_composite_runs_all_hooks tests/cli/test_run.py::test_composite_pass_parameters_to_subtasks tests/cli/test_run.py::test_composite_can_pass_parameters tests/cli/test_run.py::test_composite_only_pass_parameters_to_subtasks_with_args[with-args] tests/cli/test_run.py::test_composite_only_pass_parameters_to_subtasks_with_args[without-args] tests/cli/test_run.py::test_composite_only_pass_parameters_to_subtasks_with_args_with_default[with-args] tests/cli/test_run.py::test_composite_only_pass_parameters_to_subtasks_with_args_with_default[default] tests/cli/test_run.py::test_composite_hooks_inherit_env tests/cli/test_run.py::test_composite_inherit_env_in_cascade tests/cli/test_run.py::test_composite_inherit_dotfile tests/cli/test_run.py::test_composite_can_have_commands tests/cli/test_run.py::test_run_shortcut tests/cli/test_run.py::test_run_shortcuts_dont_override_commands tests/cli/test_run.py::test_run_shortcut_fail_with_usage_if_script_not_found tests/cli/test_run.py::test_empty_positionnal_args_still_display_usage[unknown tests/cli/test_run.py::test_empty_positionnal_args_still_display_usage[not tests/cli/test_run.py::test_empty_positional_args_display_help