Repository: facebookresearch/hydra

Issue:
Support disabling working directory management
Disabling the output directory management should be supported.
This includes:
- Not changing the working directory
- Not saving the .hydra directory
- Potentially not saving a log file

Workarounds in 1.0 / 1.1:
- Not changing the working directory: override `hydra.run.dir` to `.`
- Not saving the .hydra directory: override `hydra.output_subdir` to `null`
- Potentially not saving a log file: override hydra/job_logging to stdout.


Discussion:
Perhaps there is a less drastic option than disabling these capabilities entirely, without changing the cwd, that could be the default behavior. Hydra could still capture logs and configs and store those in the output dir. At the same time, giving the program access to this through `hydra.get_output_dir()` or `hydra.output_dir` would still allow the user to write to this directory while still assuming a non-changing cwd.

This would be more natural for new users, who are probably confused when their program can't find a data file on a relative path, while still providing them with unobtrusive logging storage and access to the output dir when they've become more familiar with hydra (there are a lot of docs to read and understand).
You don't seem to understand the proposal here.
This is not about removing any feature, but about allowing people that does not want the feature to easily not have it. (regardless of what the exact user experience is).

The idea is to support disabling it, not to kill the feature.
Enabling different control over chdir (while still generating an output directory) is definitely an option here.

> This would be more natural for new users, who are probably confused when their program can't find a data file on a relative path, while still providing them with unobtrusive logging storage and access to the output dir when they've become more familiar with hydra (there are a lot of docs to read and understand).

I know it's not related to this issue but I quite like this behavior, instead of the reverse currently.
Default to the path user is on, which is usually the path user provided from outside the code, use `hydra.output_dir()` inside the code programmatically where necessary (pass to Tensorboard logdir, Checkpoint callback etc...) 
Maybe I got up on the wrong side of the bed today, I am really sorry if it sounded angry, I didn't mean anything bad =(

I see that package is developing good, and it definitely requires a lot of effort to maintain it and make it for a lot of people. You discussed here an opportunity to better manage disabling of output logging, and I thought moving it as plugin might be a good option. But I guess it sounded like an unwanted advice and complaint about developing direction choices, and I conveyed my message wrong. Sorry, I apologize if I offended you in any way.
Apology accepted. I understand this can be frustrating.
The reason Hydra is configuring logging is related to parameter sweeps and remote execution.
When you run the same app multiple times you want to allow each instance to log to different file.

Check out this [pull request](https://github.com/facebookresearch/hydra/pull/1130) (merged into master), it can show you how to disable logging.
You can do exactly the same from your own application config. If you have question let's move them off this issue because this is not related to it (you can ask in the chat or open a separate question issue).
This will not happen for 1.1, sorry.
Changing this is a major change that should be considered in the context of other breaking changes, it will probably require a redesign of the output directory management.

Resolving patch:
diff --git a/hydra/conf/__init__.py b/hydra/conf/__init__.py
--- a/hydra/conf/__init__.py
+++ b/hydra/conf/__init__.py
@@ -46,6 +46,10 @@ class JobConf:
     # Job name, populated automatically unless specified by the user (in config or cli)
     name: str = MISSING
 
+    # Change current working dir to the output dir.
+    # Will be non-optional and default to False in Hydra 1.3
+    chdir: Optional[bool] = None
+
     # Populated automatically by Hydra.
     # Concatenation of job overrides that can be used as a part
     # of the directory name.
@@ -93,6 +97,7 @@ class RuntimeConf:
     version: str = MISSING
     cwd: str = MISSING
     config_sources: List[ConfigSourceInfo] = MISSING
+    output_dir: str = MISSING
 
     # Composition choices dictionary
     # Ideally, the value type would be Union[str, List[str], None]
diff --git a/hydra/core/utils.py b/hydra/core/utils.py
--- a/hydra/core/utils.py
+++ b/hydra/core/utils.py
@@ -118,17 +118,23 @@ def run_job(
 
     old_cwd = os.getcwd()
     orig_hydra_cfg = HydraConfig.instance().cfg
-    HydraConfig.instance().set_config(config)
-    working_dir = str(OmegaConf.select(config, job_dir_key))
+
+    output_dir = str(OmegaConf.select(config, job_dir_key))
     if job_subdir_key is not None:
         # evaluate job_subdir_key lazily.
         # this is running on the client side in sweep and contains things such as job:id which
         # are only available there.
         subdir = str(OmegaConf.select(config, job_subdir_key))
-        working_dir = os.path.join(working_dir, subdir)
+        output_dir = os.path.join(output_dir, subdir)
+
+    with read_write(config.hydra.runtime):
+        with open_dict(config.hydra.runtime):
+            config.hydra.runtime.output_dir = os.path.abspath(output_dir)
+
+    HydraConfig.instance().set_config(config)
+    _chdir = None
     try:
         ret = JobReturn()
-        ret.working_dir = working_dir
         task_cfg = copy.deepcopy(config)
         with read_write(task_cfg):
             with open_dict(task_cfg):
@@ -142,14 +148,35 @@ def run_job(
         assert isinstance(overrides, list)
         ret.overrides = overrides
         # handle output directories here
-        Path(str(working_dir)).mkdir(parents=True, exist_ok=True)
-        os.chdir(working_dir)
+        Path(str(output_dir)).mkdir(parents=True, exist_ok=True)
+
+        _chdir = hydra_cfg.hydra.job.chdir
+
+        if _chdir is None:
+            url = "https://hydra.cc/docs/upgrades/1.1_to_1.2/changes_to_job_working_dir"
+            deprecation_warning(
+                message=dedent(
+                    f"""\
+                    Hydra 1.3 will no longer change working directory at job runtime by default.
+                    See {url} for more information."""
+                ),
+                stacklevel=2,
+            )
+            _chdir = True
+
+        if _chdir:
+            os.chdir(output_dir)
+            ret.working_dir = output_dir
+        else:
+            ret.working_dir = os.getcwd()
 
         if configure_logging:
             configure_log(config.hydra.job_logging, config.hydra.verbose)
 
         if config.hydra.output_subdir is not None:
-            hydra_output = Path(config.hydra.output_subdir)
+            hydra_output = Path(config.hydra.runtime.output_dir) / Path(
+                config.hydra.output_subdir
+            )
             _save_config(task_cfg, "config.yaml", hydra_output)
             _save_config(hydra_cfg, "hydra.yaml", hydra_output)
             _save_config(config.hydra.overrides.task, "overrides.yaml", hydra_output)
@@ -172,7 +199,8 @@ def run_job(
         return ret
     finally:
         HydraConfig.instance().cfg = orig_hydra_cfg
-        os.chdir(old_cwd)
+        if _chdir:
+            os.chdir(old_cwd)
 
 
 def get_valid_filename(s: str) -> str:
diff --git a/noxfile.py b/noxfile.py
--- a/noxfile.py
+++ b/noxfile.py
@@ -91,7 +91,7 @@ def install_hydra(session, cmd):
 
 
 def pytest_args(*args):
-    ret = ["pytest", "-Werror"]
+    ret = ["pytest"]
     ret.extend(args)
     return ret
 
@@ -483,8 +483,6 @@ def test_jupyter_notebooks(session):
     args = pytest_args(
         "--nbval", "examples/jupyter_notebooks/compose_configs_in_notebook.ipynb"
     )
-    # Jupyter notebook test on Windows yield warnings
-    args = [x for x in args if x != "-Werror"]
     session.run(*args, silent=SILENT)
 
     notebooks_dir = Path("tests/jupyter")


Test patch:
diff --git a/hydra/test_utils/test_utils.py b/hydra/test_utils/test_utils.py
--- a/hydra/test_utils/test_utils.py
+++ b/hydra/test_utils/test_utils.py
@@ -342,9 +342,7 @@ def experiment(cfg):
                 expected_outputs
             ), f"Unexpected number of output lines from {task_file}, output lines:\n\n{file_str}"
             for idx in range(len(output)):
-                assert (
-                    output[idx] == expected_outputs[idx]
-                ), f"Unexpected output for {prints[idx]} : expected {expected_outputs[idx]}, got {output[idx]}"
+                assert_regex_match(expected_outputs[idx], output[idx])
         # some tests are parsing the file output for more specialized testing.
         return file_str
     finally:
diff --git a/plugins/hydra_ax_sweeper/tests/test_ax_sweeper_plugin.py b/plugins/hydra_ax_sweeper/tests/test_ax_sweeper_plugin.py
--- a/plugins/hydra_ax_sweeper/tests/test_ax_sweeper_plugin.py
+++ b/plugins/hydra_ax_sweeper/tests/test_ax_sweeper_plugin.py
@@ -205,6 +205,7 @@ def test_ax_logging(tmpdir: Path, cmd_arg: str, expected_str: str) -> None:
         "tests/apps/polynomial.py",
         "-m",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "polynomial.x=interval(-5, -2)",
         "polynomial.z=10",
         "hydra.sweeper.ax_config.max_trials=2",
@@ -227,6 +228,7 @@ def test_search_space_exhausted_exception(tmpdir: Path, cmd_args: List[str]) ->
         "tests/apps/polynomial.py",
         "-m",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.sweeper.ax_config.max_trials=2",
     ] + cmd_args
     run_python_script(cmd)
@@ -260,6 +262,7 @@ def test_jobs_using_choice_between_lists(
         "tests/apps/polynomial_with_list_coefficients.py",
         "-m",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.sweeper.ax_config.max_trials=3",
     ] + [cmd_arg]
     result, _ = run_python_script(cmd)
@@ -296,6 +299,7 @@ def test_jobs_using_choice_between_dicts(
         "tests/apps/polynomial_with_dict_coefficients.py",
         "-m",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.sweeper.ax_config.max_trials=3",
     ] + [cmd_arg]
     result, _ = run_python_script(cmd)
@@ -309,6 +313,7 @@ def test_example_app(tmpdir: Path) -> None:
         "example/banana.py",
         "-m",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "banana.x=int(interval(-5, 5))",
         "banana.y=interval(-5, 10.1)",
         "hydra.sweeper.ax_config.max_trials=2",
diff --git a/plugins/hydra_nevergrad_sweeper/tests/test_nevergrad_sweeper_plugin.py b/plugins/hydra_nevergrad_sweeper/tests/test_nevergrad_sweeper_plugin.py
--- a/plugins/hydra_nevergrad_sweeper/tests/test_nevergrad_sweeper_plugin.py
+++ b/plugins/hydra_nevergrad_sweeper/tests/test_nevergrad_sweeper_plugin.py
@@ -134,6 +134,7 @@ def test_nevergrad_example(with_commandline: bool, tmpdir: Path) -> None:
         "example/my_app.py",
         "-m",
         "hydra.sweep.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         f"hydra.sweeper.optim.budget={budget}",  # small budget to test fast
         f"hydra.sweeper.optim.num_workers={min(8, budget)}",
         "hydra.sweeper.optim.seed=12",  # avoid random failures
diff --git a/plugins/hydra_optuna_sweeper/tests/test_optuna_sweeper_plugin.py b/plugins/hydra_optuna_sweeper/tests/test_optuna_sweeper_plugin.py
--- a/plugins/hydra_optuna_sweeper/tests/test_optuna_sweeper_plugin.py
+++ b/plugins/hydra_optuna_sweeper/tests/test_optuna_sweeper_plugin.py
@@ -126,6 +126,7 @@ def test_optuna_example(with_commandline: bool, tmpdir: Path) -> None:
         "example/sphere.py",
         "--multirun",
         "hydra.sweep.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.sweeper.n_trials=20",
         "hydra.sweeper.n_jobs=1",
         f"hydra.sweeper.storage={storage}",
@@ -164,6 +165,7 @@ def test_optuna_multi_objective_example(with_commandline: bool, tmpdir: Path) ->
         "example/multi-objective.py",
         "--multirun",
         "hydra.sweep.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.sweeper.n_trials=20",
         "hydra.sweeper.n_jobs=1",
         "hydra/sweeper/sampler=random",
diff --git a/tests/jupyter/%run_test.ipynb b/tests/jupyter/%run_test.ipynb
--- a/tests/jupyter/%run_test.ipynb
+++ b/tests/jupyter/%run_test.ipynb
@@ -50,7 +50,7 @@
    "source": [
     "import tempfile\n",
     "tmpdir = tempfile.mkdtemp()\n",
-    "%run ../../examples/tutorials/basic/your_first_hydra_app/6_composition/my_app.py hydra.run.dir=$tmpdir"
+    "%run ../../examples/tutorials/basic/your_first_hydra_app/6_composition/my_app.py hydra.run.dir=$tmpdir hydra.job.chdir=True"
    ]
   }
  ],
@@ -75,4 +75,4 @@
  },
  "nbformat": 4,
  "nbformat_minor": 4
-}
+}
\ No newline at end of file
diff --git a/tests/test_apps/app_change_dir/my_app.py b/tests/test_apps/app_change_dir/my_app.py
new file mode 100644
--- /dev/null
+++ b/tests/test_apps/app_change_dir/my_app.py
@@ -0,0 +1,21 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
+import os
+from pathlib import Path
+
+from omegaconf import DictConfig
+
+import hydra
+from hydra.core.hydra_config import HydraConfig
+
+
+@hydra.main(config_path=None)
+def main(_: DictConfig):
+    subdir = Path(HydraConfig.get().run.dir) / Path("subdir")
+    subdir.mkdir(exist_ok=True, parents=True)
+    os.chdir(subdir)
+
+
+if __name__ == "__main__":
+    main()
+
+print(f"current dir: {os.getcwd()}")
diff --git a/tests/test_apps/app_with_no_chdir_override/my_app.py b/tests/test_apps/app_with_no_chdir_override/my_app.py
new file mode 100644
--- /dev/null
+++ b/tests/test_apps/app_with_no_chdir_override/my_app.py
@@ -0,0 +1,13 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
+from omegaconf import DictConfig
+
+import hydra
+
+
+@hydra.main(config_path=".")
+def my_app(_: DictConfig) -> None:
+    pass
+
+
+if __name__ == "__main__":
+    my_app()
diff --git a/tests/test_basic_sweeper.py b/tests/test_basic_sweeper.py
--- a/tests/test_basic_sweeper.py
+++ b/tests/test_basic_sweeper.py
@@ -70,6 +70,7 @@ def test_partial_failure(
         "--multirun",
         "+divisor=1,0",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.hydra_logging.formatters.simple.format='[HYDRA] %(message)s'",
     ]
     out, err = run_process(cmd=cmd, print_error=False, raise_exception=False)
diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py
--- a/tests/test_callbacks.py
+++ b/tests/test_callbacks.py
@@ -81,6 +81,7 @@ def test_app_with_callbacks(
     cmd = [
         app_path,
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra.hydra_logging.formatters.simple.format='[HYDRA] %(message)s'",
         "hydra.job_logging.formatters.simple.format='[JOB] %(message)s'",
     ]
diff --git a/tests/test_env_defaults.py b/tests/test_env_defaults.py
--- a/tests/test_env_defaults.py
+++ b/tests/test_env_defaults.py
@@ -11,5 +11,6 @@ def test_env_defaults(tmpdir: Path) -> None:
     cmd = [
         "tests/test_apps/custom_env_defaults/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     run_python_script(cmd)
diff --git a/tests/test_examples/test_advanced_config_search_path.py b/tests/test_examples/test_advanced_config_search_path.py
--- a/tests/test_examples/test_advanced_config_search_path.py
+++ b/tests/test_examples/test_advanced_config_search_path.py
@@ -37,6 +37,7 @@ def test_config_search_path(
     cmd = [
         "examples/advanced/config_search_path/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     if error is not None:
diff --git a/tests/test_examples/test_advanced_package_overrides.py b/tests/test_examples/test_advanced_package_overrides.py
--- a/tests/test_examples/test_advanced_package_overrides.py
+++ b/tests/test_examples/test_advanced_package_overrides.py
@@ -12,6 +12,7 @@ def test_advanced_package_override_simple(tmpdir: Path) -> None:
     cmd = [
         "examples/advanced/package_overrides/simple.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == {
@@ -23,6 +24,7 @@ def test_advanced_package_override_two_packages(tmpdir: Path) -> None:
     cmd = [
         "examples/advanced/package_overrides/two_packages.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == {
diff --git a/tests/test_examples/test_configure_hydra.py b/tests/test_examples/test_configure_hydra.py
--- a/tests/test_examples/test_configure_hydra.py
+++ b/tests/test_examples/test_configure_hydra.py
@@ -17,6 +17,7 @@ def test_custom_help(tmpdir: Path) -> None:
         [
             "examples/configure_hydra/custom_help/my_app.py",
             "hydra.run.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "--help",
         ]
     )
@@ -55,6 +56,7 @@ def test_job_name_no_config_override(tmpdir: Path) -> None:
     cmd = [
         "examples/configure_hydra/job_name/no_config_file_override.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert result == "no_config_file_override"
@@ -64,6 +66,7 @@ def test_job_name_with_config_override(tmpdir: Path) -> None:
     cmd = [
         "examples/configure_hydra/job_name/with_config_file_override.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert result == "name_from_config_file"
@@ -73,6 +76,7 @@ def test_job_override_dirname(tmpdir: Path) -> None:
     cmd = [
         "examples/configure_hydra/job_override_dirname/my_app.py",
         "hydra.sweep.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "learning_rate=0.1,0.01",
         "batch_size=32",
         "seed=999",
@@ -87,6 +91,7 @@ def test_logging(tmpdir: Path) -> None:
     cmd = [
         "examples/configure_hydra/logging/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert result == "[INFO] - Info level message"
@@ -96,6 +101,7 @@ def test_disabling_logging(tmpdir: Path) -> None:
     cmd = [
         "examples/configure_hydra/logging/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "hydra/job_logging=none",
         "hydra/hydra_logging=none",
     ]
@@ -106,11 +112,11 @@ def test_disabling_logging(tmpdir: Path) -> None:
 def test_workdir_config(monkeypatch: Any, tmpdir: Path) -> None:
     script = str(Path("examples/configure_hydra/workdir/my_app.py").absolute())
     monkeypatch.chdir(tmpdir)
-    result, _err = run_python_script([script])
+    result, _err = run_python_script([script, "hydra.job.chdir=True"])
     assert Path(result) == Path(tmpdir) / "run_dir"
 
     result, _err = run_python_script(
-        [script, "--multirun", "hydra/hydra_logging=disabled"]
+        [script, "--multirun", "hydra/hydra_logging=disabled", "hydra.job.chdir=True"]
     )
     assert Path(result) == Path(tmpdir) / "sweep_dir" / "0"
 
@@ -118,5 +124,11 @@ def test_workdir_config(monkeypatch: Any, tmpdir: Path) -> None:
 def test_workdir_override(monkeypatch: Any, tmpdir: Path) -> None:
     script = str(Path("examples/configure_hydra/workdir/my_app.py").absolute())
     monkeypatch.chdir(tmpdir)
-    result, _err = run_python_script([script, "hydra.run.dir=blah"])
+    result, _err = run_python_script(
+        [
+            script,
+            "hydra.run.dir=blah",
+            "hydra.job.chdir=True",
+        ]
+    )
     assert Path(result) == Path(tmpdir) / "blah"
diff --git a/tests/test_examples/test_instantiate_examples.py b/tests/test_examples/test_instantiate_examples.py
--- a/tests/test_examples/test_instantiate_examples.py
+++ b/tests/test_examples/test_instantiate_examples.py
@@ -25,6 +25,7 @@ def test_instantiate_object(tmpdir: Path, overrides: List[str], output: str) ->
     cmd = [
         "examples/instantiate/object/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ] + overrides
     result, _err = run_python_script(cmd)
     assert result == output
@@ -46,6 +47,7 @@ def test_instantiate_object_recursive(
     cmd = [
         "examples/instantiate/object_recursive/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ] + overrides
     result, _err = run_python_script(cmd)
     assert result == output
@@ -62,6 +64,7 @@ def test_instantiate_schema(tmpdir: Path, overrides: List[str], output: str) ->
     cmd = [
         "examples/instantiate/schema/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ] + overrides
     result, _err = run_python_script(cmd)
     assert result == output
@@ -90,6 +93,7 @@ def test_instantiate_schema_recursive(
     cmd = [
         "examples/instantiate/schema_recursive/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ] + overrides
     result, _err = run_python_script(cmd)
     assert_text_same(result, expected)
@@ -128,6 +132,7 @@ def test_instantiate_docs_example(
     cmd = [
         "examples/instantiate/docs_example/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ] + overrides
     result, _err = run_python_script(cmd)
     assert_text_same(result, expected)
diff --git a/tests/test_examples/test_patterns.py b/tests/test_examples/test_patterns.py
--- a/tests/test_examples/test_patterns.py
+++ b/tests/test_examples/test_patterns.py
@@ -40,6 +40,7 @@ def test_write_protect_config_node(tmpdir: Any) -> None:
     cmd = [
         "examples/patterns/write_protect_config_node/frozen.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "data_bits=10",
     ]
 
@@ -68,7 +69,11 @@ def test_extending_configs(
     monkeypatch: Any, tmpdir: Path, overrides: List[str]
 ) -> None:
     monkeypatch.chdir("examples/patterns/extending_configs")
-    cmd = ["my_app.py", "hydra.run.dir=" + str(tmpdir)] + overrides
+    cmd = [
+        "my_app.py",
+        "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
+    ] + overrides
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == {
         "db": {
@@ -105,7 +110,11 @@ def test_configuring_experiments(
     monkeypatch: Any, tmpdir: Path, overrides: List[str], expected: Any
 ) -> None:
     monkeypatch.chdir("examples/patterns/configuring_experiments")
-    cmd = ["my_app.py", "hydra.run.dir=" + str(tmpdir)] + overrides
+    cmd = [
+        "my_app.py",
+        "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
+    ] + overrides
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == expected
 
@@ -172,6 +181,10 @@ def test_multi_select(
     monkeypatch: Any, tmpdir: Path, overrides: List[str], expected: Any
 ) -> None:
     monkeypatch.chdir("examples/patterns/multi-select")
-    cmd = ["my_app.py", "hydra.run.dir=" + str(tmpdir)] + overrides
+    cmd = [
+        "my_app.py",
+        "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
+    ] + overrides
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == expected
diff --git a/tests/test_examples/test_structured_configs_tutorial.py b/tests/test_examples/test_structured_configs_tutorial.py
--- a/tests/test_examples/test_structured_configs_tutorial.py
+++ b/tests/test_examples/test_structured_configs_tutorial.py
@@ -21,6 +21,7 @@ def test_1_basic_run(tmpdir: Path) -> None:
         [
             "examples/tutorials/structured_configs/1_minimal/my_app.py",
             "hydra.run.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
         ]
     )
     assert result == "Host: localhost, port: 3306"
@@ -37,6 +38,7 @@ def test_1_basic_run_with_override_error(tmpdir: Path) -> None:
         [
             "examples/tutorials/structured_configs/1_minimal/my_app_type_error.py",
             "hydra.run.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
         ]
     )
     assert re.search(re.escape(expected), err) is not None
@@ -47,6 +49,7 @@ def test_1_basic_override(tmpdir: Path) -> None:
         [
             "examples/tutorials/structured_configs/1_minimal/my_app.py",
             "hydra.run.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "port=9090",
         ]
     )
@@ -57,6 +60,7 @@ def test_1_basic_override_type_error(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/structured_configs/1_minimal/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "port=foo",
     ]
 
@@ -76,6 +80,7 @@ def test_2_static_complex(tmpdir: Path) -> None:
         [
             "examples/tutorials/structured_configs/2_static_complex/my_app.py",
             "hydra.run.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
         ]
     )
     assert result == "Title=My app, size=1024x768 pixels"
@@ -95,6 +100,7 @@ def test_3_config_groups(tmpdir: Path, overrides: Any, expected: Any) -> None:
     cmd = [
         "examples/tutorials/structured_configs/3_config_groups/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(overrides)
     result, _err = run_python_script(cmd)
@@ -129,6 +135,7 @@ def test_3_config_groups_with_inheritance(
     cmd = [
         "examples/tutorials/structured_configs/3_config_groups/my_app_with_inheritance.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ] + overrides
     result, _err = run_python_script(cmd)
     res = OmegaConf.create(result)
@@ -139,6 +146,7 @@ def test_4_defaults(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/structured_configs/4_defaults/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == {
@@ -160,7 +168,7 @@ def test_4_defaults(tmpdir: Path) -> None:
     ],
 )
 def test_5_structured_config_schema(tmpdir: Path, path: str) -> None:
-    cmd = [path, "hydra.run.dir=" + str(tmpdir)]
+    cmd = [path, "hydra.run.dir=" + str(tmpdir), "hydra.job.chdir=True"]
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == {
         "db": {
diff --git a/tests/test_examples/test_tutorials_basic.py b/tests/test_examples/test_tutorials_basic.py
--- a/tests/test_examples/test_tutorials_basic.py
+++ b/tests/test_examples/test_tutorials_basic.py
@@ -37,6 +37,7 @@ def test_tutorial_simple_cli_app(
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/1_simple_cli/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     result, _err = run_python_script(cmd)
@@ -47,6 +48,7 @@ def test_tutorial_working_directory(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/basic/running_your_hydra_app/3_working_directory/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert result == "Working directory : {}".format(tmpdir)
@@ -63,6 +65,7 @@ def test_tutorial_logging(tmpdir: Path, args: List[str], expected: List[str]) ->
     cmd = [
         "examples/tutorials/basic/running_your_hydra_app/4_logging/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     result, _err = run_python_script(cmd)
@@ -87,6 +90,7 @@ def test_tutorial_config_file(tmpdir: Path, args: List[str], output_conf: Any) -
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/2_config_file/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     result, _err = run_python_script(cmd)
@@ -113,6 +117,7 @@ def test_tutorial_config_file_bad_key(
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/2_config_file/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     if isinstance(expected, RaisesContext):
@@ -148,6 +153,7 @@ def test_tutorial_config_groups(
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/4_config_groups/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     result, _err = run_python_script(cmd)
@@ -186,6 +192,7 @@ def test_tutorial_defaults(tmpdir: Path, args: List[str], expected: DictConfig)
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/5_defaults/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(args)
     result, _err = run_python_script(cmd)
@@ -288,6 +295,7 @@ def test_advanced_ad_hoc_composition(
     cmd = [
         "examples/advanced/ad_hoc_composition/hydra_compose_example.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == OmegaConf.create(expected)
@@ -297,6 +305,7 @@ def test_examples_using_the_config_object(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/3_using_config/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
 
     run_python_script(cmd)
diff --git a/tests/test_hydra.py b/tests/test_hydra.py
--- a/tests/test_hydra.py
+++ b/tests/test_hydra.py
@@ -70,7 +70,9 @@ def test_missing_conf_file(
 
 
 def test_run_dir() -> None:
-    run_python_script(["tests/test_apps/run_dir_test/my_app.py"])
+    run_python_script(
+        ["tests/test_apps/run_dir_test/my_app.py", "hydra.job.chdir=True"]
+    )
 
 
 @mark.parametrize(
@@ -347,6 +349,7 @@ def test_short_module_name(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/2_config_file/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     out, _err = run_python_script(cmd)
     assert OmegaConf.create(out) == {
@@ -376,6 +379,7 @@ def test_module_env_override(tmpdir: Path, env_name: str) -> None:
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/2_config_file/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     modified_env = os.environ.copy()
     modified_env[env_name] = "hydra.test_utils.configs.Foo"
@@ -392,6 +396,7 @@ def test_cfg(tmpdir: Path, flag: str, resolve: bool, expected_keys: List[str]) -
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/5_defaults/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         flag,
     ]
     if resolve:
@@ -451,6 +456,7 @@ def test_cfg_with_package(
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/5_defaults/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ] + flags
     if resolve:
         cmd.append("--resolve")
@@ -505,7 +511,12 @@ def test_cfg_with_package(
 def test_cfg_resolve_interpolation(
     tmpdir: Path, script: str, resolve: bool, flags: List[str], expected: str
 ) -> None:
-    cmd = [script, "hydra.run.dir=" + str(tmpdir), "--cfg=job"] + flags
+    cmd = [
+        script,
+        "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
+        "--cfg=job",
+    ] + flags
     if resolve:
         cmd.append("--resolve")
 
@@ -521,6 +532,7 @@ def test_resolve_flag_errmsg(tmpdir: Path, other_flag: Optional[str]) -> None:
     cmd = [
         "examples/tutorials/basic/your_first_hydra_app/3_using_config/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "--resolve",
     ]
     if other_flag is not None:
@@ -766,7 +778,7 @@ def test_sweep_complex_defaults(
 def test_help(
     tmpdir: Path, script: str, flags: List[str], overrides: List[str], expected: Any
 ) -> None:
-    cmd = [script, "hydra.run.dir=" + str(tmpdir)]
+    cmd = [script, "hydra.run.dir=" + str(tmpdir), "hydra.job.chdir=True"]
     cmd.extend(overrides)
     cmd.extend(flags)
     result, _err = run_python_script(cmd)
@@ -796,7 +808,7 @@ def test_help(
 def test_searchpath_config(tmpdir: Path, overrides: List[str], expected: str) -> None:
     cmd = ["examples/advanced/config_search_path/my_app.py"]
     cmd.extend(overrides)
-    cmd.extend(["hydra.run.dir=" + str(tmpdir)])
+    cmd.extend(["hydra.run.dir=" + str(tmpdir), "hydra.job.chdir=True"])
     result, _err = run_python_script(cmd)
     assert re.match(expected, result, re.DOTALL)
 
@@ -833,6 +845,7 @@ def test_sys_exit(tmpdir: Path) -> None:
         "-Werror",
         "tests/test_apps/sys_exit/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     assert subprocess.run(cmd).returncode == 42
 
@@ -968,6 +981,7 @@ def test_config_name_and_path_overrides(
     cmd = [
         "tests/test_apps/app_with_multiple_config_dirs/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         f"--config-name={config_name}",
         f"--config-path={config_path}",
     ]
@@ -1033,6 +1047,7 @@ def test_module_run(
     cmd = [
         directory + "/" + file,
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     modified_env = os.environ.copy()
     modified_env["PYTHONPATH"] = directory
@@ -1087,6 +1102,7 @@ def test_multirun_structured_conflict(
     cmd = [
         "tests/test_apps/multirun_structured_conflict/my_app.py",
         "hydra.sweep.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(overrides)
     if error:
@@ -1120,6 +1136,7 @@ def test_run_with_missing_default(
     ) -> None:
         cmd = cmd_base + [
             "hydra.sweep.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "--config-name=unspecified_mandatory_default",
             "--config-path=../../../hydra/test_utils/configs",
         ]
@@ -1138,6 +1155,7 @@ def test_command_line_interpolations_evaluated_lazily(
     ) -> None:
         cmd = cmd_base + [
             "hydra.sweep.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "+foo=10,20",
             "+bar=${foo}",
             "--multirun",
@@ -1155,6 +1173,7 @@ def test_multirun_config_overrides_evaluated_lazily(
     ) -> None:
         cmd = cmd_base + [
             "hydra.sweep.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "+foo=10,20",
             "+bar=${foo}",
             "--multirun",
@@ -1170,6 +1189,7 @@ def test_multirun_config_overrides_evaluated_lazily(
     def test_multirun_defaults_override(self, cmd_base: List[str], tmpdir: Any) -> None:
         cmd = cmd_base + [
             "hydra.sweep.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "group1=file1,file2",
             "--multirun",
             "--config-path=../../../hydra/test_utils/configs",
@@ -1186,6 +1206,7 @@ def test_multirun_defaults_override(self, cmd_base: List[str], tmpdir: Any) -> N
     def test_run_pass_list(self, cmd_base: List[str], tmpdir: Any) -> None:
         cmd = cmd_base + [
             "hydra.sweep.dir=" + str(tmpdir),
+            "hydra.job.chdir=True",
             "+foo=[1,2,3]",
         ]
         expected = {"foo": [1, 2, 3]}
@@ -1197,6 +1218,7 @@ def test_app_with_error_exception_sanitized(tmpdir: Any, monkeypatch: Any) -> No
     cmd = [
         "tests/test_apps/app_with_runtime_config_error/my_app.py",
         f"hydra.sweep.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     expected = dedent(
         """\
@@ -1226,6 +1248,7 @@ def test_hydra_to_job_config_interpolation(tmpdir: Any) -> Any:
     cmd = [
         "tests/test_apps/hydra_to_cfg_interpolation/my_app.py",
         "hydra.sweep.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
         "b=${a}",
         "a=foo",
     ]
@@ -1254,6 +1277,7 @@ def test_config_dir_argument(
     cmd = [
         "my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     cmd.extend(overrides)
     result, _err = run_python_script(cmd)
@@ -1265,6 +1289,7 @@ def test_schema_overrides_hydra(monkeypatch: Any, tmpdir: Path) -> None:
     cmd = [
         "my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert result == "job_name: test, name: James Bond, age: 7, group: a"
@@ -1275,6 +1300,7 @@ def test_defaults_pkg_with_dot(monkeypatch: Any, tmpdir: Path) -> None:
     cmd = [
         "my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert OmegaConf.create(result) == {
@@ -1322,7 +1348,11 @@ def test_job_exception(
     expected: str,
 ) -> None:
     ret = run_with_error(
-        ["tests/test_apps/app_exception/my_app.py", f"hydra.run.dir={tmpdir}"]
+        [
+            "tests/test_apps/app_exception/my_app.py",
+            f"hydra.run.dir={tmpdir}",
+            "hydra.job.chdir=True",
+        ]
     )
     assert_regex_match(
         from_line=expected,
@@ -1334,7 +1364,11 @@ def test_job_exception(
 
 def test_job_exception_full_error(tmpdir: Any) -> None:
     ret = run_with_error(
-        ["tests/test_apps/app_exception/my_app.py", f"hydra.run.dir={tmpdir}"],
+        [
+            "tests/test_apps/app_exception/my_app.py",
+            f"hydra.run.dir={tmpdir}",
+            "hydra.job.chdir=True",
+        ],
         env={**os.environ, "HYDRA_FULL_ERROR": "1"},
     )
 
@@ -1347,6 +1381,7 @@ def test_structured_with_none_list(monkeypatch: Any, tmpdir: Path) -> None:
     cmd = [
         "my_app.py",
         "hydra.run.dir=" + str(tmpdir),
+        "hydra.job.chdir=True",
     ]
     result, _err = run_python_script(cmd)
     assert result == "{'list': None}"
@@ -1359,7 +1394,7 @@ def test_self_hydra_config_interpolation_integration(tmpdir: Path) -> None:
         task_config=cfg,
         overrides=[],
         prints="HydraConfig.get().job_logging.handlers.file.filename",
-        expected_outputs="task.log",
+        expected_outputs=r".+task.log$",
     )
 
 
@@ -1378,6 +1413,7 @@ def test_hydra_main_without_config_path(tmpdir: Path) -> None:
     cmd = [
         "tests/test_apps/hydra_main_without_config_path/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     _, err = run_python_script(cmd, allow_warnings=True)
 
@@ -1397,10 +1433,33 @@ def test_hydra_main_without_config_path(tmpdir: Path) -> None:
     )
 
 
+def test_job_chdir_not_specified(tmpdir: Path) -> None:
+    cmd = [
+        "tests/test_apps/app_with_no_chdir_override/my_app.py",
+        f"hydra.run.dir={tmpdir}",
+    ]
+    _, err = run_python_script(cmd, allow_warnings=True)
+
+    expected = dedent(
+        """
+        .*UserWarning: Hydra 1.3 will no longer change working directory at job runtime by default.
+        See https://hydra.cc/docs/upgrades/1.1_to_1.2/changes_to_job_working_dir for more information..*
+        .*
+        """
+    )
+    assert_regex_match(
+        from_line=expected,
+        to_line=err,
+        from_name="Expected error",
+        to_name="Actual error",
+    )
+
+
 def test_app_with_unicode_config(tmpdir: Path) -> None:
     cmd = [
         "tests/test_apps/app_with_unicode_in_config/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     out, _ = run_python_script(cmd)
     assert out == "config: 数据库"
@@ -1424,6 +1483,7 @@ def test_frozen_primary_config(
     cmd = [
         "examples/patterns/write_protect_config_node/frozen.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     cmd.extend(overrides)
     ret, _err = run_python_script(cmd)
@@ -1463,6 +1523,7 @@ def test_hydra_deprecation_warning(
     cmd = [
         "tests/test_apps/deprecation_warning/my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
     ]
     env = os.environ.copy()
     if env_deprecation_err:
@@ -1471,3 +1532,41 @@ def test_hydra_deprecation_warning(
         cmd, env=env, allow_warnings=True, print_error=False, raise_exception=False
     )
     assert_multiline_regex_search(expected, err)
+
+
+@mark.parametrize(
+    "multirun,expected",
+    [
+        (False, ["my_app.log", ".hydra/config.yaml"]),
+        (True, ["0/my_app.log", "0/.hydra/config.yaml", "multirun.yaml"]),
+    ],
+)
+def test_disable_chdir(tmpdir: Path, multirun: bool, expected: List[str]) -> None:
+    cmd = [
+        "examples/tutorials/basic/running_your_hydra_app/3_working_directory/my_app.py",
+        f"hydra.run.dir={tmpdir}",
+        f"hydra.sweep.dir={tmpdir}",
+        "hydra.job.chdir=False",
+    ]
+    if multirun:
+        cmd += ["-m"]
+    result, _err = run_python_script(cmd)
+    assert f"Working directory : {os.getcwd()}" in result
+    for p in expected:
+        path = os.path.join(str(tmpdir), p)
+        assert Path(path).exists()
+
+
+@mark.parametrize(
+    "chdir",
+    [True, False],
+)
+def test_disable_chdir_with_app_chdir(tmpdir: Path, chdir: bool) -> None:
+    cmd = [
+        "tests/test_apps/app_change_dir/my_app.py",
+        f"hydra.run.dir={tmpdir}",
+        f"hydra.job.chdir={chdir}",
+    ]
+    result, _err = run_python_script(cmd)
+    _path = os.getcwd() if chdir else Path(tmpdir) / "subdir"
+    assert f"current dir: {_path}" in result
diff --git a/tools/configen/tests/test_generate.py b/tools/configen/tests/test_generate.py
--- a/tools/configen/tests/test_generate.py
+++ b/tools/configen/tests/test_generate.py
@@ -289,6 +289,7 @@ def test_example_application(monkeypatch: Any, tmpdir: Path):
     cmd = [
         "my_app.py",
         f"hydra.run.dir={tmpdir}",
+        "hydra.job.chdir=True",
         "user.name=Batman",
     ]
     result, _err = run_python_script(cmd)