- use
pnpm dev:execfor evaluating scripts using BashEnv during development. See Debugging info below. - Install packages via pnpm rather than editing package.json directly
- Bias towards making new test files that are roughly logically grouped rather than letting test files gets too large. Try to stay below 300 lines. Prefer making a new file when you want to add a
describe() - Prefer asserting the full STDOUT/STDERR output rather than using toContain or not.toContain
- Always also add
comparison-testsfor major command functionality, but edge cases should always be covered in unit tests which are mush faster (pnpm test:comparison) - When you are unsure about bash/command behavior, create a
comparison-teststest file to ensure compat. --helpdoes not need to pass comparison tests and should reflect actual capability- Commands must handle unknown arguments correctly
- Always ensure all tests pass in the end and there are no compile and lint errors
- Use
pnpm lint:fix - Always also run
pnpm knip - Strongly prefer running a temporary comparison test or unit test over an ad-hoc script to figure out the behavior of some bash script or API.
- The implementation should align with the real behavior of bash, not what is convenient for TS or TE tests.
- Always make sure to build before using dist
- Biome rules often have the same name as eslint rules (if you are lookinf for one)
- Error / show usage on unknown flags in commands and built-ins (unless real bash also ignores)
- Dependencies that use wasm are not allowed (exception: sql.js for SQLite, approved for security sandboxing). Binary npm packages are fine
- When you implement multiple tasks (such as multiple commands or builtins or discovered bugs), so them one at a time, create tests, validate, and then move on
- Running tests does not require building first
- Don't use
cat > test-direct.ts << 'SCRIPT'style test scripts because they constantly require 1-off approval. - Instead use
pnpm dev:exec- use
--real-bashto also get comparison output from the system bash - use
--print-astto also print the AST of the program as parsed by our parser.ts
- use
- Must have usage statement
- Must error on unknown options (unless bash ignores them)
- Must have extensive unit tests collocated with the command
- Should have comparison tests if there is doubt about behavior
- We explicitly don't support 64bit integers
- Must never hang. All parsing and execution should have reasonable max limits to avoid runaway compute.
User-controlled data (stdin, arguments, file content, HTTP headers, environment variables) can become JavaScript object keys. To prevent prototype pollution attacks:
-
Always use
Object.create(null)for objects with user-controlled keys:// BAD - vulnerable to prototype pollution const obj: Record<string, string> = {}; obj[userKey] = value; // userKey could be "__proto__" or "constructor" // GOOD - safe from prototype pollution const obj: Record<string, string> = Object.create(null); obj[userKey] = value; // null-prototype prevents prototype chain access
-
Use
Map<string, T>instead of plain objects when possible - Maps don't have prototype pollution issues. -
Use helper functions from
src/helpers/env.ts:mapToRecord()- safely converts Map to null-prototype RecordmapToRecordWithExtras()- same but merges extra propertiesmergeToNullPrototype()- safely merges objects
-
Use safe-object utilities from
src/commands/query-engine/safe-object.ts:isSafeKey()- checks if key is safe (not__proto__,constructor,prototype)safeSet()- sets property only if key is safesafeFromEntries()- creates null-prototype object from entriesnullPrototypeCopy(obj)- creates a null-prototype shallow copy of an objectnullPrototypeMerge(...objs)- merges objects into a new null-prototype object
-
Prefer
nullPrototypeCopyover object spread for user data:// BAD - spread creates object with Object.prototype const copy = { ...userObject }; // GOOD - null-prototype copy const copy = nullPrototypeCopy(userObject); // BAD - merging user objects const merged = { ...objA, ...objB }; // GOOD - null-prototype merge const merged = nullPrototypeMerge(objA, objB);
- HTTP header parsing (curl, fetch responses)
- CSV/JSON/YAML parsing where keys come from data
- Command argument parsing
- Environment variable handling
- AWK/jq variable and array storage
Add prototype pollution tests for any code that stores user-controlled keys:
- Test with keywords:
constructor,__proto__,prototype,hasOwnProperty,toString,valueOf - Verify
Object.prototypeis not modified after processing - See existing tests in
src/interpreter/prototype-pollution.test.tsandsrc/commands/*/prototype-pollution.test.ts