I would like to set the environment variables for a spawned subprocess only. They are controlled by process-environment. The documentation says:

binding that variable with let is also reasonable practice.

In this example I try to unset HOME with:

(defun f()
  (let ((old-home (getenv  "HOME")))
    (let* ((process-environment process-environment)
       (process-environment (setenv-internal process-environment  "HOME" nil nil)))
      (start-process "proc" nil "notepad"))
    (string= old-home (getenv  "HOME"))))


(f) returns nil and process-environment is permanently changed (outside the let*).

Another thing, that happens when debugging, is that concurrent processes using HOME (for example Emacs autosave hooks) might fail crashing the debug session.

An alterative would be to use two setenvs before and after start-process. It still would not solve the conflict with concurrent processes (and does not explain how to use process-environment inside a let).

3 Answers 3


setenv-internal and setenv change the list stored in process-environment (as local or special variable) by side-effects.

It does not help if you assign the list (pointer) to a local variable process-environment. You still have only a single list for the process-environment which is just bound to two variables -- the global variable process-environment and the let-bound variable process-environment. If you modify that single list by side-effect, it effects all processes that use that list as process environment regardless to which variables the list is bound.

To get separate process environments for all processes you must create a copy of process-environment for the let-binding, e.g. with cl-copy-list.

(defun f()
    (let ((old-home (getenv  "HOME"))
      (process-environment (cl-copy-list process-environment)))
      (setenv "HOME" nil)
      (start-process "proc" (current-buffer) "emacs" "--batch" "--eval" "(prin1 (concat \"proc gets \" (getenv-internal \"HOME\" initial-environment)))" "--kill")
      (message "HOME within the let: %s" (getenv  "HOME")))
    (message "HOME after the let: %s" (getenv "HOME"))
  • Thank you very much. I still wonder if I can have the setenv affecting only the given process and avoid conflicts with concurrent processes. For example, consider the autosave hook starting while HOME is unset/modified and the save path uses ~.
    – antonio
    Commented Jun 1, 2018 at 7:38
  • @antonio I have added some sentences that hopefully make crystal clear that cl-copy-list avoid effects across processes through process-environment.
    – Tobias
    Commented Jun 1, 2018 at 20:00
  • 1
    (getenv-internal \"HOME\" initial-environment) won't really give the initial environment value of HOME on Windows prior to Emacs 25.2 (see Bug#10980).
    – npostavs
    Commented Jun 1, 2018 at 20:25

Since Emacs 28.1 you can use the with-environment-variables macro.


(with-environment-variables (("DESKTOP_SESSION" "TEST_VALUE"))
  (shell-command "echo $DESKTOP_SESSION"))
(shell-command "echo $DESKTOP_SESSION")



The "let binding" way is the following (notice that contrary to Tobias's answer, this does not involve any copying or "set"ting):

(let ((process-environment
       (cons "HOME"
             (cons (concat "OLDHOME=" (getenv "HOME"))
  (start-process "proc" (current-buffer) ...))
  • 3
    It should be noted that the (cons "HOME" part only has the desired effect in Emacs 25.1 or newer, due to Bug#23779.
    – npostavs
    Commented May 31, 2018 at 18:58
  • 1
    @npostavs Thanks. I had almost the same solution as Stefan (only the OLDHOME-thing was a bit different) but I discarded it because it did not work for me. Your comment disclosed why.
    – Tobias
    Commented May 31, 2018 at 19:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.