Why this matters
A project often needs more than one command running at once. A server runs in one terminal and keeps running. Tests run in a second terminal when you change code. A Git command runs in a third. Typing the same long command again and again is slow and easy to get wrong.
This lesson covers two ways to make that smoother. A profile sets up what a new terminal starts with: which shell, which extra settings. A task is a command saved under a name, so you run it from a list or with a key. The build task from the running-code lesson is one example of a task. Here you write tasks for a test run and for a check that runs two commands one after the other.
Several terminals are several programs
A terminal is a window that talks to a shell. A shell is a program that reads the commands you type and runs them, such as bash on Linux or PowerShell on Windows. Each terminal in VS Code starts its own shell. The shells do not share anything unless you make them share it.
Here is a small test. In the first command, a child shell changes its folder and prints it. The second command, in your own shell, prints the folder again:
bash -c 'cd /usr; pwd'
pwdOutput:
/usr
/home/chiranjeet/dev/anisha/scratch-offtmp/claude-vscode-lessonsThe cd changed the folder inside the child shell only. Your shell stayed where it was, so the second path is your own folder and will differ on your machine. The same holds for variables, which are named values a shell keeps in memory. A variable set in one shell is not visible in another:
bash -c 'export GREETING=hi; echo $GREETING'
echo "[$GREETING]"Output:
hi
[]The first shell printed hi. The second printed nothing between the brackets, because it never had the variable. This is why each terminal in VS Code is a separate place. A folder you changed to in one terminal is not changed in the next one, and a variable you set in one is missing from the others.
Keep the server in one terminal, and type the other commands in a second.
What a profile is
A profile is a named recipe for a terminal. It answers three questions:
- Which program starts? This is the
pathfield, for examplebashor the full path tobash.exe. - Which extra options does it pass to that program? This is the
argsfield, a list. Each item in the list is one option. - Which variables are set before the shell starts? This is the
envfield, an object. Each pair is a variable name and its value.
The drop-down arrow in the terminal panel, from the running-code lesson, lists your profiles. Profiles live in the settings file, which the first lesson of this module introduced. The two settings that matter are:
terminal.integrated.profiles.linux(or.windows, or.osx) holds the list of profiles for that system.terminal.integrated.defaultProfile.linux(or.windows, or.osx) holds the name of the profile a new terminal uses.
Here is a profile for Linux that starts bash with one extra variable. The variable is named PROJECT_ENV and is set to dev:
{
"terminal.integrated.profiles.linux": {
"project shell": {
"path": "bash",
"env": {
"PROJECT_ENV": "dev"
}
}
},
"terminal.integrated.defaultProfile.linux": "project shell"
}The name in quotes, project shell, is the name you see in the drop-down. The default profile line must use that name exactly, with the same spaces and capital letters.
To see what the env field does, run the same thing outside VS Code. The env command sets a variable for one program, and then that program can read it:
env PROJECT_ENV=dev bash -c 'echo $PROJECT_ENV'Output:
devVS Code does the equivalent with the env field before the shell starts, so every command you type in that terminal sees PROJECT_ENV.
Two profiles for two projects
Suppose one machine runs two projects, one against a development database and one against a test database. Each project can have its own profile, and the drop-down lists both. The settings look like this:
{
"terminal.integrated.profiles.linux": {
"dev shell": {
"path": "bash",
"env": { "PROJECT_ENV": "dev" }
},
"test shell": {
"path": "bash",
"env": { "PROJECT_ENV": "test" }
}
},
"terminal.integrated.defaultProfile.linux": "dev shell"
}Each profile sets the same variable to a different value. A program in the project reads PROJECT_ENV and picks the matching database. You can check each value the same way as before:
env PROJECT_ENV=dev bash -c 'echo $PROJECT_ENV'
env PROJECT_ENV=test bash -c 'echo $PROJECT_ENV'Output:
dev
testOpening the test shell profile from the drop-down gives a terminal where PROJECT_ENV is test. The default profile is used only when you open a new terminal without picking one.
Choosing the default with a command
You do not have to type the name in the settings file. Open the Command Palette and run Terminal: Select Default Profile. A list of profiles appears. Pick one, and VS Code writes the default profile line for you.
A Windows profile
On Windows, the usual choice for a Unix-style shell is Git Bash, which the previous lesson showed in the drop-down. The file path to its program has backslashes, and in JSON a backslash has a special meaning, so each one is written twice:
{
"terminal.integrated.profiles.windows": {
"Git Bash": {
"path": "C:\\Program Files\\Git\\bin\\bash.exe",
"args": ["--login", "-i"]
}
},
"terminal.integrated.defaultProfile.windows": "Git Bash"
}The --login option makes Git Bash start as a login shell.
Keep profiles in your own settings
A profile contains paths that only exist on one machine. C:\\Program Files\\Git exists on a Windows laptop, and not on a Mac or a Linux server. Put profiles in your user settings, not in the project's .vscode/settings.json, which the whole team shares. A shared profile with a Windows path breaks everyone else's terminal.
Tasks: commands with a name
A task is a command saved in .vscode/tasks.json. VS Code can run it from the Command Palette, from the Terminal menu, or from a key. The file is part of the project, so you can commit it to Git, and the whole team gets the same tasks.
The file has one list called tasks. Each task is an object with these fields:
labelis the name you see in the task list. Other parts of the file refer to a task by its label.typeisshell, which means VS Code runs the command in a shell.commandis the program to run, such asnode.argsis a list of the arguments, one item per argument. Writing them as a list means you do not have to think about spaces or quotes inside the command.groupsays what kind of task this is. The valuetestmakes the task show up when you run Tasks: Run Test Task. The valuebuildis what the running-code lesson used withCtrl+Shift+B.optionssets where the task starts.${workspaceFolder}is a placeholder that VS Code replaces with the path of the project's root folder.presentationsets how the output is shown."panel": "dedicated"means the task keeps one terminal for itself and reuses it on later runs."panel": "new"opens a fresh terminal each time.
The examples below use a small project with this layout:
tasks-demo/
.vscode/tasks.json
src/sum.js
test/sum.test.jsThe function in src/sum.js adds up the numbers in a list:
function sum(numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
module.exports = { sum };The test file checks two cases, with the test runner built into Node:
const test = require('node:test');
const assert = require('node:assert');
const { sum } = require('../src/sum');
test('sum adds numbers', () => {
assert.strictEqual(sum([1, 2, 3]), 6);
});
test('sum of empty list is 0', () => {
assert.strictEqual(sum([]), 0);
});Worked example 1: a test task with its own key
Put this in .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "run tests",
"type": "shell",
"command": "node",
"args": ["--test"],
"group": "test",
"options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always" }
}
]
}node --test runs the test files it finds in the project. It runs files whose names end in .test.js, and every .js file inside a test folder. The command is the one you would type yourself in a terminal, so you can check it there first. Here is what it prints in this project:
✔ sum adds numbers (2.514977ms)
✔ sum of empty list is 0 (0.32107ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 88.48743The timings change from run to run. The line ℹ fail 0 is what matters. The task shows this same text, because it runs the same command in the same folder.
To give the task a key, open the Keyboard Shortcuts page's JSON file as the previous lesson showed, and add:
[
{
"key": "ctrl+alt+t",
"command": "workbench.action.tasks.runTask",
"args": "run tests"
}
]The command workbench.action.tasks.runTask runs a task by its label. The args field gives the label, so "run tests" must match the label in tasks.json exactly. Pressing Ctrl+Alt+T now runs the tests.
Worked example 2: one task that runs others
Often a check is more than one command. You want the code to be syntactically valid, and the tests to pass, in that order. Write each step as its own task, then a third task that lists them:
{
"version": "2.0.0",
"tasks": [
{
"label": "syntax check",
"type": "shell",
"command": "node",
"args": ["--check", "src/sum.js"],
"options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always" }
},
{
"label": "run tests",
"type": "shell",
"command": "node",
"args": ["--test"],
"group": "test",
"options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always" }
},
{
"label": "check all",
"dependsOn": ["syntax check", "run tests"],
"dependsOrder": "sequence",
"group": "build"
}
]
}The third task has no command. It only lists two other tasks in dependsOn, so running it runs them. The dependsOrder value sequence makes them run one after the other. If you write parallel instead, they start at the same time, which is only safe when the tasks do not depend on each other.
The node --check command reads a file and reports syntax errors, without running it. On a file with no errors it prints nothing and exits with code 0. You can run it yourself to see that:
node --check src/sum.js
echo "exit code $?"Output:
exit code 0The exit code is the number the program gives back when it finishes. Zero means success. Any other number means something failed. The task system reads the same number.
Worked example 3: a task that fails, and where to look
Suppose you type the name of a file wrong in the task. Here is node --check pointed at a file that does not exist:
node --check src/missing.jsOutput, shortened to the first lines and the error line:
node:internal/modules/cjs/loader:1568
throw err;
^
Error: Cannot find module '/home/chiranjeet/dev/anisha/scratch-offtmp/claude-vscode-lessons/tasks-demo/src/missing.js'Read the Error line first. It names the file that was not found, with its full path. The path shown is the folder the command ran in, which is why options.cwd matters. Your path will differ. If the path in the message is not the folder you expected, the task started in the wrong place.
Worked example 4: a failing test and its exit code
Tests fail when the code is wrong, and a task has to report that. Put a test with a wrong expected value in test/broken.test.js. It expects sum([1, 2]) to be 4, but the answer is 3:
const test = require('node:test');
const assert = require('node:assert');
const { sum } = require('../src/sum');
test('sum adds two numbers wrongly', () => {
assert.strictEqual(sum([1, 2]), 4);
});Run the file and then print the exit code:
node --test test/broken.test.js
echo "exit code $?"Output, with the middle lines removed:
✖ sum adds two numbers wrongly (4.391206ms)
ℹ tests 1
ℹ pass 0
ℹ fail 1
...
✖ failing tests:
test at test/broken.test.js:5:1
✖ sum adds two numbers wrongly (4.391206ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
3 !== 4
...
exit code 1The line 3 !== 4 says the value the code produced, 3, is not the value the test expected, 4. The exit code is 1, not 0, so any task that runs this command knows it failed. Fix the code or the test, then run it again. Delete test/broken.test.js once you have seen the result, because node --test runs every file in test/, and the run tests task would keep failing.
Common mistakes
A single backslash in a Windows path
Here is the profile path written with single backslashes:
{
"path": "C:\Program Files\Git\bin\bash.exe"
}JSON reads a backslash followed by a letter as the start of a special sequence, and \P is not one. Node's JSON parser reports this for the same text:
SyntaxError: Bad escaped character in JSON at position 16 (line 2 column 15)Write the path with doubled backslashes, as in the Windows example above.
A command name with a typo
A task whose command is nodee fails with the shell's own message. In a bash terminal it reads:
bash: line 1: nodee: command not foundThe exit code is 127, which the shell uses for "command not found". Look for the exact text of the command in the task's terminal output, and fix the name in tasks.json.
A dependsOn label that does not match
The text in dependsOn must match a task's label letter for letter, with the same capitals and spaces. "Run tests" does not match "run tests". If the label is wrong, VS Code cannot find that task, so the composite task cannot run it.
A test task without the group
A task that runs tests but has no "group": "test" does not appear under Tasks: Run Test Task. You can still run it from the task list, or with a key that names its label.
A profile that only works on your machine
A profile with a Windows path, added to the project's .vscode/settings.json, reaches every team member's copy of the project. On their machine the path does not exist, and the terminal fails to start. Keep machine-specific profiles in your user settings.
Practice
Exercise 1. On Linux, add a profile named project shell that starts bash and sets the variable PROJECT_ENV to dev. Make it the default profile. Write the settings.
Show the answer
{
"terminal.integrated.profiles.linux": {
"project shell": {
"path": "bash",
"env": {
"PROJECT_ENV": "dev"
}
}
},
"terminal.integrated.defaultProfile.linux": "project shell"
}Exercise 2. Add a task named check test file to the tasks list. It runs node --check on the test file test/sum.test.js. Set the working folder to the project root.
Show the answer
{
"label": "check test file",
"type": "shell",
"command": "node",
"args": ["--check", "test/sum.test.js"],
"options": { "cwd": "${workspaceFolder}" }
}Exercise 3. The check all task has "dependsOrder": "sequence". Someone changes it to "parallel". What changes when the task runs?
Show the answer
Both tasks start at the same time, and the syntax check does not wait for the tests to finish. The result of the check can then appear before or after the test output, in no fixed order. Use sequence when the second step must not start until the first has finished.
Exercise 4. Add a key, Ctrl+Alt+R, that runs the task named run tests. Write the keybindings file.
Show the answer
[
{
"key": "ctrl+alt+r",
"command": "workbench.action.tasks.runTask",
"args": "run tests"
}
]Exercise 5. Fix this profile so the file is valid JSON and the path is correct. The Git Bash program is at C:\Program Files\Git\bin\bash.exe.
{
"terminal.integrated.profiles.windows": {
"Git Bash": {
"path": "C:\Program Files\Git\bin\bash.exe"
}
}
}Show the answer
The backslashes must be doubled inside the JSON text:
{
"terminal.integrated.profiles.windows": {
"Git Bash": {
"path": "C:\\Program Files\\Git\\bin\\bash.exe"
}
}
}Exercise 6. The test run in worked example 4 printed ℹ fail 1. What exit code did the command give back, and what should a task runner do with it?
Show the answer
The exit code is 1. A task runner reads any value other than 0 as a failure, and the zero value means success.
Summary
- Each terminal runs its own shell. A folder or variable set in one terminal is not set in another.
- A profile sets the program, the arguments and the environment variables for a new terminal. The default profile line must match a profile name exactly.
- Keep profiles that contain machine-specific paths in your user settings, not in the shared project file.
- A task in
tasks.jsonhas alabel, acommand, and a list ofargs. Thegroupfield decides which task list it appears in. dependsOnwithdependsOrderruns several tasks in sequence, or in parallel.- A task runs in the folder its
options.cwdsets, and its output is the same text you see when you run the command yourself.