Skip to main content

Inspect and edit files from the terminal with Node.js and Python

Need to check one setting in a large file, inspect a few lines around an error, or update a JSON version without opening an editor? A short terminal command can help. Node.js and Python are especially useful when the change needs logic: parse JSON, calculate a value, validate it, and write the result.

This guide focuses on WSL/Linux with Bash, then shows Windows PowerShell and CMD equivalents. You do not need a separate npm package for the Node.js examples or a Python package for the Python examples.

1. Understand where the command runs​

The shell starts a program; Node or Python runs the code you pass to it:

node -e 'console.log("Hello from JavaScript")'
python3 -c 'print("Hello from Python")'

-e means “evaluate this JavaScript”; -c means “execute this Python code.” In Bash, the outer single quotes protect the code from shell expansion. Double quotes inside the code then belong to JavaScript or Python. Shell quoting rules differ in PowerShell and CMD; do not assume a Bash one-liner can be pasted unchanged into those shells.

JavaScript and Python: the same job in two languages​

TaskNode.js / JavaScriptPython
Run inline codenode -e '...'python3 -c '...'
Read a text filefs.readFileSync(path, "utf8")Path(path).read_text(encoding="utf-8")
Parse JSON textJSON.parse(text)json.loads(text)
Serialize a value as JSONJSON.stringify(value, null, 2)json.dumps(value, indent=2)
Write text to a filefs.writeFileSync(path, text, "utf8")Path(path).write_text(text, encoding="utf-8")
Print a valueconsole.log(value)print(value)
Run a saved programnode scripts/update-version.jspython3 scripts/update_version.py

Choose the language you and your team can maintain. For a repository that already uses Node.js, JavaScript can be a convenient choice; Python is equally capable. fs is Node's built-in file-system module, while Path comes from Python's standard pathlib module.

Start by checking your location and available tools:

pwd
ls -la
node --version
python3 --version
npm --version

A relative path like package.json is resolved from pwd, not necessarily from the folder where a script was saved. In WSL, a Windows file such as C:\work\app\package.json is usually available as /mnt/c/work/app/package.json; quote paths containing spaces. Node and Python must be installed inside the environment in which you run them.

2. Inspect first, edit second​

The simplest tools are often the best for plain text:

cat package.json                     # whole small file
less README.md # scroll; press q to quit
head -n 20 README.md # first 20 lines
tail -n 20 README.md # last 20 lines
sed -n '40,65p' src/app.ts # lines 40–65, inclusive
nl -ba src/app.ts | sed -n '40,65p' # show line numbers too

Search by content or file name with rg (ripgrep), if installed:

rg -n 'versionName' .                  # matching lines with numbers
rg -n -C 3 'versionName' src # three context lines on each side
rg -n -F 'api.example.com' src # literal text, not a regular expression
rg --files -g '*.json' # matching file names

By default rg respects ignore files and skips hidden files; use rg --hidden only when you intend to include them. If ripgrep is unavailable, grep -n 'versionName' file.json is a basic alternative. Avoid printing secrets to a shared terminal or log.

The same line range in Node and Python​

Line numbers below are one-based and both ends are inclusive. For small text files:

node -e 'const fs = require("node:fs"); const lines = fs.readFileSync("src/app.ts", "utf8").split(/\r?\n/); for (let i = 39; i < Math.min(65, lines.length); i++) console.log(`${i + 1}: ${lines[i]}`);'
python3 -c 'from pathlib import Path; lines = Path("src/app.ts").read_text(encoding="utf-8").splitlines(); [print(f"{i}: {line}") for i, line in enumerate(lines, 1) if 40 <= i <= 65]'

These examples load the entire file into memory. For large logs, use sed, head, tail, or stream a file instead. For example, this Python command stops after line 65:

python3 -c 'import itertools; f = open("src/app.ts", encoding="utf-8"); print("".join(itertools.islice(f, 39, 65)), end=""); f.close()'

3. Read JSON as data, not as text​

Assume android-version.json contains:

{
"versionName": "1.4.9",
"versionCode": 21
}

Read just one property in Node or Python:

node -e 'const fs = require("node:fs"); const d = JSON.parse(fs.readFileSync("android-version.json", "utf8")); console.log(d.versionName);'
python3 -c 'import json; from pathlib import Path; d = json.loads(Path("android-version.json").read_text(encoding="utf-8")); print(d["versionName"])'

For a nested value, follow the property path, for example d.scripts.build in JavaScript or d["scripts"]["build"] in Python. Missing properties and malformed JSON should be investigated instead of silently producing a new configuration. These examples expect valid JSON, not JSON with comments or trailing commas.

Inspect package.json with npm​

Run from the package directory:

npm pkg get author private scripts
npm pkg get 'scripts.build'

npm pkg get reads the local package.json and prints selected values as JSON. It is an npm command, not a general JSON reader. For workspaces, npm can select packages with its workspace options; check npm help pkg for the installed npm version.

4. A safe workflow for a write​

Commands that call writeFileSync, write_text, Set-Content, or redirect with > can overwrite data. Work on a known file and verify the outcome:

  1. Check the path and current content with pwd, ls, and a read-only command.
  2. Check for existing changes with git status --short and git diff -- path/to/file if the file is tracked. Preserve changes already made by other people or tools.
  3. Copy important files to a new, non-existing backup path or commit your work first. For example, after checking that android-version.json.bak does not exist, use cp -n android-version.json android-version.json.bak. -n avoids overwriting a backup.
  4. Preview the proposed value before writing it. Confirm that only the intended file is targeted.
  5. Apply the change once, inspect the file, and compare with git diff -- android-version.json (or diff -u android-version.json.bak android-version.json).

Do not use recursive edits or overwrite generated, shared, or production files without knowing the consequences. A script can fail partway through a write; for important files use a tested temporary-file-and-rename workflow, backups, and appropriate concurrency controls.

5. Increment a JSON version with Node.js​

Suppose the intended rule is “increase the last numeric component of a dot-separated version.” Thus 1.4.9 becomes 1.4.10. This is a simple numeric rule, not a complete semantic versioning implementation: prereleases such as 1.4.9-beta are deliberately rejected below. For example, 1.0.24 becomes 1.0.25; this is an illustration, not a statement about any repository's current version. The JSON property versionCode is not incremented by this command. Check your project's release process to determine whether it must change separately.

For a LocalTell checkout in WSL, first navigate to its actual root—for example, if it is on the Windows C: drive at this location:

cd /mnt/c/AR_Files/code/local-tell
pwd
cat android-version.json

If your checkout is elsewhere, use that path instead. Check the file's current value rather than relying on a version number in an old example.

First preview the result without writing:

node -e 'const fs = require("node:fs"); const d = JSON.parse(fs.readFileSync("android-version.json", "utf8")); const s = d.versionName; if (typeof s !== "string" || !/^\d+(\.\d+)*$/.test(s)) throw new Error("Expected dot-separated numeric versionName"); const v = s.split("."); v[v.length - 1] = String(BigInt(v[v.length - 1]) + 1n); console.log(`${s} -> ${v.join(".")}`);'

After reviewing and making a backup, apply it:

node -e 'const fs = require("node:fs"); const p = "android-version.json"; const d = JSON.parse(fs.readFileSync(p, "utf8")); const s = d.versionName; if (typeof s !== "string" || !/^\d+(\.\d+)*$/.test(s)) throw new Error("Expected dot-separated numeric versionName"); const v = s.split("."); v[v.length - 1] = String(BigInt(v[v.length - 1]) + 1n); d.versionName = v.join("."); fs.writeFileSync(p, JSON.stringify(d, null, 2) + "\n", "utf8"); console.log("New versionName:", d.versionName);'

How to build this command:

  1. readFileSync reads UTF-8 text; JSON.parse turns it into a JavaScript object.
  2. Validate the property, then split(".") makes an array of components.
  3. BigInt(...) + 1n increments the last component without floating-point precision loss.
  4. join(".") builds the new version; JSON.stringify(d, null, 2) formats the object.
  5. writeFileSync replaces the entire file, and the last newline keeps the file tidy.

To see the same logic without squeezing it onto one line, here is the JavaScript portion as a readable program. Do not run it until you have inspected and backed up the target file:

const fs = require('node:fs');
const path = 'android-version.json';
const data = JSON.parse(fs.readFileSync(path, 'utf8'));
const current = data.versionName;

if (typeof current !== 'string' || !/^\d+(\.\d+)*$/.test(current)) {
throw new Error('Expected dot-separated numeric versionName');
}

const parts = current.split('.');
const last = parts.length - 1;
parts[last] = String(BigInt(parts[last]) + 1n);
data.versionName = parts.join('.');

fs.writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
console.log('New versionName:', data.versionName);

This is the same sequence as the one-liner: read → parse → validate → change → serialize → write. If saved as scripts/update-version.cjs, run it from the repository root with node scripts/update-version.cjs. The .cjs extension ensures that require works even if the project configures .js files as ES modules.

After applying either version of the command, verify both properties and the file diff:

cat android-version.json
git diff -- android-version.json

The diff shows tracked changes relative to the Git index, so it may also include edits that were already present. Compare against the backup to isolate your latest edit. If the file is untracked, git diff will not show its contents; inspect the file and compare it with the backup using diff -u instead.

The command reformats all JSON and normalizes line endings, so the diff may show more than the changed property. It also removes leading zeros from the last component. It is not safe to run twice accidentally: each run increments again. Two simultaneous writers can also lose updates. For a project with versioning rules or concurrent releases, use its release tooling.

6. Do the same thing with Python​

Preview:

python3 -c 'import json,re; from pathlib import Path; d=json.loads(Path("android-version.json").read_text(encoding="utf-8")); s=d["versionName"]; assert isinstance(s,str) and re.fullmatch(r"[0-9]+(?:\.[0-9]+)*",s), "Expected dot-separated numeric versionName"; v=s.split("."); v[-1]=str(int(v[-1])+1); print(s,"->",".".join(v))'

Apply after checking the preview and making a backup:

python3 -c 'import json,re; from pathlib import Path; p=Path("android-version.json"); d=json.loads(p.read_text(encoding="utf-8")); s=d["versionName"]; assert isinstance(s,str) and re.fullmatch(r"[0-9]+(?:\.[0-9]+)*",s), "Expected dot-separated numeric versionName"; v=s.split("."); v[-1]=str(int(v[-1])+1); d["versionName"]=".".join(v); p.write_text(json.dumps(d,indent=2,ensure_ascii=False)+"\n",encoding="utf-8"); print("New versionName:",d["versionName"])'

Path.read_text and write_text deal with text; json.loads and json.dumps convert between JSON text and Python values. v[-1] is the last list item. This example uses assert as a compact interactive check; do not rely on assert for production validation because Python can disable assertions in optimized mode. Like the Node version, this rewrites the JSON file.

7. More useful programming recipes​

node -e 'const fs = require("node:fs"); fs.readFileSync("README.md", "utf8").split(/\r?\n/).forEach((line, i) => { if (line.includes("install")) console.log(`${i + 1}: ${line}`); });'
python3 -c 'from pathlib import Path; lines=Path("README.md").read_text(encoding="utf-8").splitlines(); [print(f"{i}: {line}") for i,line in enumerate(lines,1) if "install" in line]'

These searches are case-sensitive and literal. Prefer rg -n -F 'install' README.md for routine searching; use code when you need extra conditions or structured output.

List JSON object keys​

node -e 'const fs = require("node:fs"); console.log(Object.keys(JSON.parse(fs.readFileSync("package.json", "utf8"))).join("\n"));'
python3 -c 'import json; from pathlib import Path; d=json.loads(Path("package.json").read_text(encoding="utf-8")); print("\n".join(d.keys()))'

Validate JSON without changing it​

node -e 'const fs = require("node:fs"); JSON.parse(fs.readFileSync("package.json", "utf8")); console.log("Valid JSON");'
python3 -m json.tool package.json > /dev/null && echo 'Valid JSON'

The redirection sends the pretty-printed JSON to /dev/null, not back to the input file. A parse error causes a nonzero exit status. Never redirect a program's output to its own input file: the shell can truncate it before the program reads it.

Preview a text replacement​

node -e 'const fs = require("node:fs"); const s = fs.readFileSync("README.md", "utf8"); console.log(s.replaceAll("old.example.com", "new.example.com"));'

This prints a proposed result; it does not edit the file. Review occurrences first with rg -n -F 'old.example.com' README.md. If you later write a replacement, check the expected match count, keep a backup, and verify the diff. Plain text replacement is not a substitute for parsing structured formats.

Pass a file name as an argument​

Keeping a path separate from code avoids constructing code from user input:

node -e 'const fs = require("node:fs"); console.log(fs.readFileSync(process.argv[1], "utf8"));' -- 'my folder/notes.txt'
python3 -c 'import sys; from pathlib import Path; print(Path(sys.argv[1]).read_text(encoding="utf-8"), end="")' 'my folder/notes.txt'

The quotes make the path one shell argument. For Node -e, the first supplied argument is process.argv[1]; for Python -c, it is sys.argv[1]. Never paste untrusted text into code and then evaluate it.

8. When a one-liner becomes a script​

One-liners are convenient for a quick, local task. Once you need branches, error handling, tests, or repeated use, put the code in a named .js or .py file, review it, and run it with node script.js or python3 script.py. Add a dry-run option, clear usage, and a nonzero exit code for invalid input. A readable script is easier to review and safer to share.

9. PowerShell and CMD equivalents​

PowerShell has its own object pipeline and quoting rules. The following examples use PowerShell syntax, not Bash syntax:

Get-Location
Get-Content -LiteralPath 'README.md' -TotalCount 20
Get-Content -LiteralPath 'README.md' | Select-Object -Skip 39 -First 26
Select-String -LiteralPath 'README.md' -Pattern 'install' -SimpleMatch -Context 3,3
Get-Content -LiteralPath 'package.json' -Raw | ConvertFrom-Json | Select-Object author, private, scripts
npm pkg get author private scripts

-Skip 39 -First 26 selects lines 40–65. -Raw reads JSON as one string. PowerShell's ConvertFrom-Json produces objects; ConvertTo-Json can rewrite a file but may change formatting, ordering, and encoding. Preview and back up before using Set-Content.

For Node/Python code in PowerShell, prefer a small script file for anything nontrivial. Native argument quoting varies between Windows PowerShell and newer PowerShell releases. A simple PowerShell-native inspection is often easier:

$data = Get-Content -LiteralPath 'android-version.json' -Raw | ConvertFrom-Json
$data.versionName

In classic Windows CMD, type README.md prints a file and findstr /n /c:"install" README.md finds literal lines. npm pkg get author private scripts works there too. CMD uses different escaping rules again; for longer Node/Python code, use a script file rather than adapting Bash single-quoted one-liners by guesswork.

Quick reference​

GoalUseful starting point
Inspect one rangesed -n '40,65p' file
Find text and contextrg -n -C 3 -F 'text' file
Inspect package metadatanpm pkg get author private scripts
Parse JSON in JavaScriptJSON.parse(fs.readFileSync(path, 'utf8'))
Parse JSON in Pythonjson.loads(Path(path).read_text(encoding='utf-8'))
Apply a calculated editValidate, preview, back up, write, then inspect the diff

The key habit is simple: read and validate first, change one known file, then verify.

10. node -p vs node -e vs npm pkg​

All three can inspect a package name, but they are different tools—not interchangeable in every situation:

CommandPurposeWhen to use it
node -p 'expression'Evaluate a JavaScript expression and print its result.A quick calculation or reading one JSON property.
node -e 'code'Execute JavaScript code; print explicitly with console.log.Multiple statements, conditionals, transformations, or file operations.
npm pkg get fieldRead fields from a package's package.json using npm.Inspect npm package metadata, scripts, or workspace packages.

For example, from a repository root, inspect the name in a built package:

node -p "require('./dist/libs/test/package.json').name"
node -e "console.log(require('./dist/libs/test/package.json').name)"

-p is short for --print; -e is short for --eval. The first prints the expression's value automatically; the second requires console.log to display it. require can load JSON directly. The file path is resolved relative to the evaluated code's current working directory; check with pwd. require caches loaded modules within a Node process, so if a long-running program edits and rereads the same file, prefer reading it explicitly with fs and parsing it.

For npm, target the package directory explicitly, for example by running npm inside it:

(cd ./dist/libs/test && npm pkg get name)

Parentheses create a Bash subshell, so the original terminal stays in its current directory. Or, from the package directory itself, run npm pkg get name. npm prints a JSON value such as "test", while the Node examples print the string test without JSON quotes. Structured values, missing fields, output formatting, and exit behavior can also differ; these are similar ways to inspect a value, not byte-for-byte equivalents. npm pkg get requires a valid npm package and operates on its package.json; it is not a general-purpose reader for arbitrary JSON files.

Node and Python can read an arbitrary JSON file directly:

node -p 'JSON.parse(require("node:fs").readFileSync("android-version.json", "utf8")).versionName'
node -e 'const fs = require("node:fs"); const d = JSON.parse(fs.readFileSync("android-version.json", "utf8")); console.log(d.versionName);'
python3 -c 'import json; from pathlib import Path; print(json.loads(Path("android-version.json").read_text(encoding="utf-8"))["versionName"])'

Do not confuse npm pkg get with npm run: the former reads the declared script value; the latter executes a script. npm also has commands that modify package metadata, so check the subcommand before running it.

11. Check which files exist at a location​

These examples are read-only. Substitute your actual path and quote paths with spaces:

ls -la 'dist/libs/test'                  # list this directory
ls -ld 'dist/libs/test' # inspect the directory itself
test -f 'dist/libs/test/package.json' && echo 'File exists'
test -d 'dist/libs/test' && echo 'Directory exists'
rg --files 'dist/libs/test' # files visible to ripgrep, recursively
find 'dist/libs/test' -maxdepth 2 -type f -name '*.json'

test -f checks for a regular file; test -d checks for a directory. A missing item makes test return a nonzero exit status. rg --files observes ignore and hidden-file rules, so it is not an exhaustive inventory by default; find searches the specified tree, including hidden entries, subject to permissions. ls includes hidden entries with -a.

Node and Python can check a path too:

node -e 'const fs = require("node:fs"); console.log(fs.existsSync("dist/libs/test/package.json") ? "Exists" : "Missing");'
python3 -c 'from pathlib import Path; p=Path("dist/libs/test/package.json"); print("File exists" if p.is_file() else "Missing or not a regular file")'

existsSync only checks existence: use fs.statSync(path).isFile() when you need to distinguish a file from a directory, and handle missing paths. A file can change between checking and using it, so real programs must still handle errors during the actual read or write.

12. Replace exactly one complete line​

Suppose settings.txt contains a line that is exactly old setting and should become new setting. The following examples reject zero or multiple matches, replace the complete line only, and preserve existing LF or CRLF line endings. They write to the file: inspect it, create a backup, and review the values first. Do not use these for binary files or as a substitute for parsing JSON, YAML, or another structured format.

Node.js, as a multiline Bash heredoc:

node <<'NODE'
const fs = require('node:fs');
const path = 'settings.txt';
const oldLine = 'old setting';
const newLine = 'new setting';
const original = fs.readFileSync(path, 'utf8');
const lines = original.split(/(\r?\n)/); // Keep the line separators.
const matches = lines.filter((part, index) => index % 2 === 0 && part === oldLine).length;
if (matches !== 1) throw new Error(`Expected one matching line; found ${matches}`);
for (let i = 0; i < lines.length; i += 2) {
if (lines[i] === oldLine) lines[i] = newLine;
}
fs.writeFileSync(path, lines.join(''), 'utf8');
console.log('Replaced one line in', path);
NODE

Python equivalent, preserving line endings by working with bytes:

python3 - <<'PY'
from pathlib import Path

path = Path('settings.txt')
old_line = b'old setting'
new_line = b'new setting'
original = path.read_bytes()
lines = original.splitlines(keepends=True)

def content(line):
return line.removesuffix(b'\n').removesuffix(b'\r')

matches = sum(content(line) == old_line for line in lines)
if matches != 1:
raise ValueError(f'Expected one matching line; found {matches}')

updated = []
for line in lines:
body = content(line)
ending = line[len(body):]
updated.append((new_line if body == old_line else body) + ending)
path.write_bytes(b''.join(updated))
print('Replaced one line in', path)
PY

The Python example uses Python 3.9 or later for removesuffix. Its byte values are UTF-8 compatible for the ASCII strings shown; encode non-ASCII values explicitly. Both examples assume ordinary LF/CRLF text; test unusual encodings and line endings separately. Afterwards:

git diff -- settings.txt
diff -u settings.txt.bak settings.txt # if you created this backup

The commands write in place, not atomically; they are not suitable for concurrent writers.

13. Multiline programs directly in Bash: heredocs​

A here-document, or heredoc, sends multiple lines to a command's standard input. With a quoted delimiter such as <<'PY', Bash does not expand $variables, backticks, or command substitutions inside the body. The terminating word must appear alone, with no indentation. These examples are Bash/WSL syntax, not PowerShell or CMD syntax.

Update a JSON number with Python​

This changes versionCode to 31 and leaves the versionName value unchanged. It overwrites the JSON file and reformats it, so check the current values and create a backup first:

python3 - <<'PY'
import json
from pathlib import Path

path = Path('android-version.json')
data = json.loads(path.read_text(encoding='utf-8'))
data['versionCode'] = 31
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
print(path.read_text(encoding='utf-8'))
PY

Equivalent JavaScript using Node's standard input:

node <<'NODE'
const fs = require('node:fs');
const path = 'android-version.json';
const data = JSON.parse(fs.readFileSync(path, 'utf8'));
data.versionCode = 31;
fs.writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
console.log(fs.readFileSync(path, 'utf8'));
NODE

31 is a JSON number, not the string "31". The example sets a fixed value; choose the value required by your release process. Python's and Node's serializers may format or escape JSON slightly differently. Both rewrite the entire file. Always inspect the diff.

Update a JSON string using Python file objects​

This alternative uses json.load and json.dump directly with file handles. Opening with w truncates the file, so it is a write operation and needs the same backup and review:

python3 - <<'PY'
import json

path = 'android-version.json'
with open(path, encoding='utf-8') as file:
data = json.load(file)

data['versionName'] = '1.0.22' # Example only; choose your intended version.
with open(path, 'w', encoding='utf-8') as file:
json.dump(data, file, indent=2, ensure_ascii=False)
file.write('\n')
PY

The equivalent complete Node program is:

node <<'NODE'
const fs = require('node:fs');
const path = 'android-version.json';
const data = JSON.parse(fs.readFileSync(path, 'utf8'));
data.versionName = '1.0.22'; // Example only; choose your intended version.
fs.writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
NODE

Both examples overwrite the file. json.load(file) reads from a file object, while json.loads(text) reads a string; dump writes to a file and dumps returns a string.

Create a multiline text file​

The shell can send literal text to cat, whose output is redirected into a file:

cat > /tmp/example-release-notes.md <<'EOF'
# Example release notes

Initial geographic data release.

## Coverage

- 9 province packs
- 25 districts

The text `$HOME` and `backticks` remain literal in this quoted heredoc.
EOF

> creates the file or truncates an existing one immediately. First check the exact path and ensure it does not exist; use set -C (Bash noclobber) or a different new path if you must prevent accidental overwrite. /tmp is temporary and may be cleaned by the system; choose a project path for material that must be retained. >> appends instead of replacing, but can duplicate content if repeated. Inspect with cat /tmp/example-release-notes.md.

14. What do these command names mean?​

NameMeaning or originCommon use
sedStream editorSelect or transform text streams, e.g. sed -n '40,65p' file.
nlNumber linesPrint text with line numbers; -ba numbers blank lines too.
rgCommand name for ripgrepSearch files quickly; it is not a separate shell feature.
grepGlobal regular expression printSearch for lines matching a pattern.
catConcatenatePrint or combine files; it can also receive standard input.
lsListList directory entries.
pwdPrint working directoryShow the shell's current directory.
cpCopyCopy a file or directory.
diffDifferenceCompare files; -u produces a unified diff.
head / tailFirst / last part of inputInspect the beginning or end of a file.
findFind filesystem entriesSearch a directory tree by name, type, and other conditions.

rg is the executable name of ripgrep, not an official expansion that needs to be memorized. Its defaults exclude ignored and hidden content; deliberately opt in when your task needs it.

15. WSL paths, realpath, and finding skill files​

Windows paths, WSL paths, and application settings do not always follow the same rules. A directory such as C:\repos\ai-automation\ar_solutions\skills is commonly available in WSL at /mnt/c/repos/ai-automation/ar_solutions/skills. The actual Windows path, drive mount, WSL user, and application environment can differ. Run these examples in the WSL terminal that owns the files; substitute your own path.

See the full path with realpath​

pwd
printf 'Home: %s\n' "$HOME"
realpath .
realpath /mnt/c/repos/ai-automation/ar_solutions/skills

realpath resolves a path to an absolute, canonical path, processing . and .. and resolving symbolic links. Depending on options and implementation, missing components may cause an error; for an existing directory, check that the path exists and is accessible first. realpath verifies filesystem resolution—not whether an application accepts, reads, or trusts that directory.

In a Bash command, ~ at the start of an unquoted path expands to your home directory:

printf '%s\n' ~/projects
printf '%s\n' "$HOME/projects" # useful when a path may contain spaces

Do not write "~/projects" expecting Bash to expand ~: inside quotes it stays literal. Use "$HOME/projects" when quoting is needed. This is a shell rule; ~ inside a JSON string is just text unless the application interprets it itself.

Understand ~/../../mnt/c​

If—and only if—your WSL home is /home/actionanand, this path resolves as follows:

~                         -> /home/actionanand
~/.. -> /home
~/../.. -> /
~/../../mnt/c -> /mnt/c

Check your own result rather than assuming the same home-directory depth:

printf 'HOME=%s\n' "$HOME"
realpath ~/../../mnt/c/repos/ai-automation/ar_solutions/skills
realpath /mnt/c/repos/ai-automation/ar_solutions/skills

If both commands print the same path, the filesystem resolves them to the same location. This does not prove that a VS Code setting will accept or discover skills there. A path such as ~/../../mnt/c/... has been used as a workaround in some setups, but it relies on the home location and the application's handling of ~ and ... Treat it as an experiment, not a portable or officially guaranteed configuration. WSL, local Windows, and remote VS Code sessions may each have different home directories.

Find only the files you need​

Given this structure:

skills/
├── documentation-writer/
│ └── SKILL.md
└── project-documentation/
└── SKILL.md

Use find with a quoted name pattern:

SKILLS="/mnt/c/repos/ai-automation/ar_solutions/skills"
ls -la "$SKILLS"
find "$SKILLS" -maxdepth 2 -type f -name 'SKILL.md' -print
sed -n '1,15p' "$SKILLS/documentation-writer/SKILL.md"
sed -n '1,15p' "$SKILLS/project-documentation/SKILL.md"

The starting directory is depth 0, each skill folder is depth 1, and its SKILL.md is depth 2. -type f selects regular files, -name matches the file name, and -print prints paths. Quote '*.md' or 'SKILL.md' so Bash does not expand the pattern before find sees it. For example:

find "$SKILLS" -maxdepth 2 -type f -name '*.md' -print
find "$HOME/.copilot/skills" -maxdepth 2 -type f -name 'SKILL.md' -print

If the user-level directory does not exist yet, find reports an error; check with test -d "$HOME/.copilot/skills" first. find normally does not follow directory symbolic links; do not add links or use -L blindly. Inspect the target and the application's policy.

Filesystem discovery versus VS Code skill discovery​

An absolute /mnt/c/... path may exist in WSL yet be rejected by an application setting. Conversely, a setting that accepts a path does not guarantee that it loads a skill. The previously used chat.agentSkillsLocations setting is now deprecated and applies to the VS Code Local harness, not the Copilot harness; support can change with VS Code versions. The VS Code agent skills guide documents supported workspace and user locations, and the migration guide explains moving custom-location skills to supported locations. Check those guides for your installed version and selected agent before changing settings.

For troubleshooting:

  1. Verify the path and the first lines of each SKILL.md with the commands above.
  2. Confirm that VS Code is connected to the intended WSL environment and agent/harness; its home directory may differ from the terminal you tested.
  3. Check the skill's required front matter, folder name, and discovery location against the current VS Code documentation. In supported versions, /skills opens the skill configuration menu. Reload the window if needed and inspect the available skills.
  4. If the files exist but do not appear, investigate skill loading separately from filesystem path resolution. Avoid claiming a particular workaround or slash command will always work.

Copy skills into a user-level directory, deliberately​

If you choose the documented user-level ~/.copilot/skills location, copying creates separate copies: later edits to the original and copied folders do not synchronize. Review the source skill files first, including any referenced scripts, because a skill can instruct an agent to take actions. Before copying, check for an existing destination and do not overwrite or merge someone else's skill folder.

SKILLS="/mnt/c/repos/ai-automation/ar_solutions/skills"
DEST="$HOME/.copilot/skills"
ls -la "$SKILLS"
ls -ld "$DEST" 2>/dev/null || printf 'Destination does not exist yet\n'

After verifying the source and destination, create the parent directory and copy only if the named destination does not already exist:

mkdir -p "$DEST"
for skill in documentation-writer project-documentation; do
if [ ! -d "$SKILLS/$skill" ]; then
printf 'Missing source: %s\n' "$SKILLS/$skill" >&2
elif [ -e "$DEST/$skill" ] || [ -L "$DEST/$skill" ]; then
printf 'Skipping existing destination: %s\n' "$DEST/$skill" >&2
else
cp -a "$SKILLS/$skill" "$DEST/"
fi
done
find "$DEST" -maxdepth 2 -type f -name 'SKILL.md' -print

mkdir -p creates missing parent directories; cp -a copies each directory and preserves attributes where supported. This check helps avoid accidental replacement in ordinary use; it is not an atomic safeguard against another process changing the destination concurrently. Verify the copies and then use the supported VS Code discovery workflow. No copying is required just to inspect paths.