task icon indicating copy to clipboard operation
task copied to clipboard

Add option to ignore environment variables in Taskfile variable resolution

Open szpn opened this issue 9 months ago • 7 comments

Description

We would like to propose adding an option to remove environment variables from the variable resolution precedence in Taskfiles.

Today, environment variables can unintentionally override Taskfile defaults, even when no value was explicitly passed.
This breaks predictability: a developer's local environment can silently affect task behavior, leading to hard-to-debug issues - especially in large teams.

Example context:
We use Taskfile in our monorepository to simplify development with easy-to-remember tasks.
Many tasks define user-optional variables with default values, for example:

tasks:
  db:dump:search:
    vars:
        ...
        USER: '{{.USER | default "local"}}'
        ...
    cmds: ...

Developers typically run tasks like:

$ task db:dump:search USER=user123

or simply:

$ task db:dump:search

In the second case, we expect the default value local to be used.
However, if the developer has a USER environment variable set on their machine, it overrides our intended default, even though the user did not pass anything.
This leads to confusing and unexpected task behavior.

Another real-world scenario:
Suppose a developer is troubleshooting something and temporarily sets a production AWS profile locally:

export AWS_PROFILE=prod

They run a few manual AWS CLI commands - but then forget about the export.
Later, they run a Task that's meant to safely connect to staging by default:

tasks:
  aws:exec:
    vars:
      AWS_PROFILE: '{{.AWS_PROFILE | default "staging"}}'
    cmds: ...

Running:

$ task aws:exec

unexpectedly connects to production, because the AWS_PROFILE environment variable silently overrides the Taskfile's intended safe default (staging).
This could lead to serious operational mistakes - like modifying production infrastructure when the developer thought they were working against staging.

Currently, Task resolves variables in this order (highest priority first):

  1. Variables declared directly in the task definition
  2. Variables passed when calling a task from another task
  3. Variables from the included Taskfile (for included tasks)
  4. Variables from the inclusion of the Taskfile
  5. Global variables (defined in vars: section)
  6. Environment variables

There is no way to opt-out of the environment fallback.

Proposal:
Introduce a setting (global or per-variable) to disable environment variable fallback during variable resolution.
This would ensure that developer-provided arguments and Taskfile defaults are always respected first - making task behavior fully predictable, safer, and environment-independent.

szpn avatar Apr 28 '25 15:04 szpn

@szpn You should be able to craft something with templating (if/else/end) and the env function. https://taskfile.dev/reference/templating/#os-functions

LIke this: {{if .VAR and not env VAR}} {{ .VAR}} {{else}} {{default_val}} {{end}}

trulede avatar Apr 28 '25 17:04 trulede

I was having a similar problem but with env instead of var

For example given the following task

  testenv:
    env:
      AWS_PROFILE: "test"
    cmds:
      - echo "Running AWS command with profile env $AWS_PROFILE"

An environment variable set in the user's shell will override the one defined in the task itself.

For example the following prints prod but I was expecting it to print test

export AWS_PROFILE=prod

❯ task testenv
task: [testenv] echo "Running AWS command with profile env $AWS_PROFILE"
Running AWS command with profile env prod

Was the behavior always like that? I was surprised i didn't stumble on this sooner

In my case I am reading a secret that the user is supposed to have exported in his shell already. For now i am using different environment variable names to avoid conflicts. For example:

  do_stuff_with_elasticsearch_staging:
    requires:
      vars: ["ES_API_KEY_STAGING"]
    env:
      ES_API_KEY:
        sh: echo $ES_API_KEY_STAGING
    cmds:
    ....

If this is a different issue to what is described in the description let me know and ill move it elsewhere!

Cheers

KostasKgr avatar Apr 29 '25 08:04 KostasKgr

After some more reading I found https://github.com/go-task/task/issues/993 which is exactly like my comment above, and there seems to be an experiment about variables https://github.com/go-task/task/issues/2034, https://taskfile.dev/experiments/env-precedence, which addresses the behavior in my comment.

This experiment does not seem to address how you try to use defaults USER: '{{.USER | default "local"}}'

KostasKgr avatar Apr 29 '25 08:04 KostasKgr

@szpn You should be able to craft something with templating (if/else/end) and the env function. https://taskfile.dev/reference/templating/#os-functions

LIke this: {{if .VAR and not env VAR}} {{ .VAR}} {{else}} {{default_val}} {{end}}

Tried that approach, but unfortunately we couldn't get it to work as expected.

It's a clever idea, but maintaining custom if/else logic across tasks doesn’t scale well - especially in a large monorepo. What we’re really looking for is a built-in way to disable environment variable fallback, so Taskfile defaults are always respected unless explicitly overridden.

This experiment does not seem to address how you try to use defaults USER: '{{.USER | default "local"}}'

Correct - I’ve tested it, and unfortunately the issue with the default keyword still persists, even with the experiment enabled.

szpn avatar Apr 29 '25 09:04 szpn

@szpn The templating is quite useful, and allows powerful constructs. I would look for a solution that does not use if/else/end ... it might be a simple as FOO: '{{if not env "FOO" | .FOO | default "bar" }}'.

Otherwise, you can start task with a clean environment, that would do the same thing. On linux its ...

env -i task

Lots of options before you need more options.

Edit: You might also discover that not having any environment variables makes the rest of you task file non operative (e.g. no PATH). In that case, the fine-grained-control offered by templating would be more suitable.

trulede avatar Apr 29 '25 15:04 trulede

@szpn you might also try a .env file, so rather than ignore envars, actively set them to the value you want.

https://taskfile.dev/usage/#env-files

trulede avatar Apr 30 '25 05:04 trulede

@szpn The templating is quite useful, and allows powerful constructs. I would look for a solution that does not use if/else/end ... it might be a simple as FOO: '{{if not env "FOO" | .FOO | default "bar" }}'.

In our case (specifically with USER), templating doesn’t solve the problem.

USER: '{{if not (env "USER")}}{{.USER | default "local"}}{{else}}{{.USER}}{{end}}'

The issue is: USER is almost always set by the OS (Linux, macOS, etc.), so even when the user doesn’t pass anything, .USER still picks up the environment value. That means the default ("local") is never used

Otherwise, you can start task with a clean environment, that would do the same thing. On linux its ...

env -i task

Lots of options before you need more options.

Edit: You might also discover that not having any environment variables makes the rest of you task file non operative (e.g. no PATH). In that case, the fine-grained-control offered by templating would be more suitable.

env -i task does technically work, but it's too blunt for most cases. It wipes everything, including critical vars like PATH, which can break basic commands unless manually restored. Not really practical.

Same goes for .env files - we’re not looking to disable or wipe the whole environment. We just want Taskfile vars to ignore those values, and stick to their default unless explicitly passed.

What we really need is variable control - something like this: "Use this default unless the user explicitly passes something - and ignore the environment."

We've also tried various workarounds, for example globally setting var:

vars:
  USER: '' # workaround to make sure USER from system env is not used

at the top of the taskfile. With it, the expression

      USER: '{{.USER | default "local"}}'

works as expected(the value is "local" when no parameter is passed, and correctly sets the value when passed), but has a bug #2218 which makes it unusable for our use case

szpn avatar May 05 '25 09:05 szpn

You can "ignore" envars with the following method:

version: '3'

env:
  USER: ''
vars:
  USER: '{{.USER | default "local"}}'

tasks:
  default:
    env:
      USER: '{{.USER}}'
    cmds:
      - echo $USER 
      - echo {{.USER}}

depending on exactly what you want, then run task this way:

$ task
task: [default] echo $USER
someuser
task: [default] echo local
local
$ TASK_X_ENV_PRECEDENCE=1 task
task: [default] echo $USER
local
task: [default] echo local
local
$ TASK_X_ENV_PRECEDENCE=1 task USER=foo
task: [default] echo $USER
foo
task: [default] echo foo
foo

More information about the TASK_X_ENV_PRECEDENCE experiment.

trulede avatar Dec 06 '25 13:12 trulede