Library / SDK
sindresorhus/escape-goat avatar
sindresorhus/escape-goat

escape-goat: a two-function HTML escaping package for JavaScript

&🐐; Escape a string for use in HTML or the inverse

555 stars20 forksJavaScriptMIT

At a glance

What is it?
escape-goat is a tiny MIT licensed package with exactly two functions, htmlEscape and htmlUnescape, each of which also works as a tagged template literal so interpolated values are escaped automatically. The README is blunt about its limits: it is safe in normal tags and unsafe inside script and style.
Who is it for?
escape-goat suits a project that needs one specific thing, escaping and unescaping the five standard HTML entities, and wants it done by a two-function package with a tagged template form that makes accidental omission hard.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 92 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two functions and nothing else

escape-goat is described in one line: escape a string for use in HTML or the inverse. That is the entire feature set, and the package manifest confirms it. The published files are index.js and index.d.ts and nothing else, the exports field points at a single ES module entry, and there is no dependencies list at all in package.json, only devDependencies for the test tooling.

The API is two functions. htmlEscape(string) escapes the characters `&` `<` `>` `"` `'`. htmlUnescape(htmlString) reverses it, unescaping the entities `&amp;` `&lt;` `&gt;` `&quot;` `&#39;`.

The escape set is the standard five. That is worth pausing on, because the interesting question with any escaping package is not what it escapes but what it does not. It does not escape the forward slash, it does not touch backticks, and it does not escape equals signs or parentheses. It escapes exactly the characters that can terminate or break out of an HTML text node or a quoted attribute value, and nothing more.

The package is ESM only, with type set to module and engines requiring Node 12 or newer. It carries sideEffects false, which tells a bundler it is safe to tree-shake, and it ships TypeScript declarations with a type test file, index.test-d.ts, checked by the tsd runner.

The author is Sindre Sorhus and the licence is MIT.

The tagged template form, and why it matters

Each function also works as a tagged template literal, and that is the part of the API that changes how you use it.

js
import {htmlEscape, htmlUnescape} from 'escape-goat';

htmlEscape('Hello <em>World</em>');
//=> 'Hello &lt;em&gt;World&lt;/em&gt;'

As a tag, the function receives the template's static parts and the interpolated values separately, so it can escape the values while leaving the static parts of your markup alone:

This is the design point of the package. With a plain function you have to remember to call it on every value, and forgetting one is an XSS bug. With the tag form, the escaping is bound to the interpolation syntax, so a value cannot slip through unescaped by accident. The same applies to htmlUnescape, which unescapes the interpolated values of its template.

The README's tip reinforces the habit: ensure you always quote your HTML attributes to prevent possible XSS. That is, quote the attribute in your own markup as well as escaping the value, since an unquoted attribute has different rules about which characters break out of it.

The XSS boundary the README draws

The README includes a warning that any honest evaluation has to take seriously. This method of escaping is only safe when inserting data into normal tags like body, div, p, b, td and so on. Inserting htmlEscape'd data into tags like script and style opens your app to XSS vulnerabilities, and the link goes to the OWASP cheat sheet on cross site scripting prevention.

The reason is straightforward once you see it. Inside a script element the content is not HTML text, it is JavaScript source, and the sequence `</script>` inside a string literal ends the element regardless of escaping, since the HTML parser does not understand JavaScript string quoting. The five characters this package escapes do not include the slash, so `</script>` passes through untouched. The same applies to a style element, where the content is CSS rather than text.

So the package's guarantee is precisely scoped: it makes values safe for text nodes and for quoted attribute values in ordinary HTML, and it says so. That is a narrower guarantee than a sanitiser gives and a much more honest one than most escaping helpers claim, since it names the contexts where its output is unsafe rather than implying its output is always safe.

The distinction matters when you pick a tool. A sanitiser parses the input as HTML and removes or rewrites anything dangerous, so it works across contexts but changes the content. An escaper transforms characters in a string and leaves the structure alone, so it works in one context and does not attempt the other.

Installing it and using it for the first time

Installation is a single npm command:

sh
npm install escape-goat

The package is ESM only, so in an existing CommonJS project you would need a dynamic import or an interop shim rather than a require call. The import form shown in the README is the intended usage:

js
import {htmlEscape, htmlUnescape} from 'escape-goat';

The first thing to try is escaping a string that contains an ampersand, which is the simplest case that shows the shape of the output. Then unescape it again and confirm the round trip returns the original. The ampersand case is worth doing first because it is the one that catches implementations which forget to escape `&` first and end up double-unescaping their own output.

A sensible first use in a real codebase is a template that interpolates user-controlled values into an attribute, since that is where the tag form pays off immediately and where the README's tip about always quoting attributes applies. The url example in the README shows the shape: the static markup in the template stays readable, and the interpolated value comes back with its quotes turned into entities.

The package does not provide a way to escape for a URL, a CSS context or a JavaScript string. If your value is going into one of those, this is the wrong function and you need a context-specific encoder.

The FAQ answer and the alternative approaches

The README carries a single FAQ entry, and it explains the project's existence better than any feature list could. Asked why yet another HTML escaping package, the answer is that the author could not find one they liked that was tiny, well-tested, and had both escape and unescape methods.

Three criteria, and the second one is the interesting constraint. A general sanitiser library is not tiny. A tiny helper is often not well-tested. And most of the small helpers escape but do not unescape, which means you cannot write a round-trip test or decode content that arrived escaped.

The alternative approaches are worth naming concretely, because the difference is in the guarantee rather than in the API shape. A sanitiser such as DOMPurify takes untrusted markup and returns markup that is safe to insert, by parsing and rebuilding it. That is the right tool when the input is arbitrary HTML. The difference in approach is that a sanitiser changes structure and can remove content, while escape-goat changes characters and preserves structure exactly.

A second alternative is doing it by hand with replace calls, or using the browser's own escaping through a DOM element's textContent. The hand-written version is what escape-goat replaces, and it is worth remembering that the naive implementation usually forgets `&` or forgets `'`.

A third is escaping on the server, in a template engine. That is a different architecture rather than a different package, and it moves the problem rather than solving it, since the escaping context is determined by where the value ends up, which the server often cannot know.

Package hygiene and the maintenance picture

The package manifest is small and tells you most of what matters about maintenance. The test script runs xo and ava and tsd, so linting, unit tests and type tests are one command. The devDependencies are ava at 3.15, tsd at 0.14 and xo at 0.38, all from the same period, which dates the tooling.

The repository layout matches a very small package: index.js, index.d.ts, index.test-d.ts, test.js, a package.json, a readme, a license file, a logo.jpg, an .editorconfig, a .npmrc and a .github directory. There is no build step and no bundler configuration, which for a package with no dependencies and two functions is the right amount of machinery.

The release history is where the detail sits. The tags are v4.0.0 from 2021-04-16, v3.0.0 from 2020-01-15 and v2.1.1 from 2019-09-23, and the v4 major bump is the ESM conversion, since the manifest is type module with a single exports entry. The last push to the repository was on 2026-07-23, so the tree is being touched well after the last tagged release.

Read that combination carefully. A package with zero dependencies, two pure functions and no platform surface has almost nothing to break, so a slow release cadence is a much smaller signal here than it would be for a library with native bindings or a service client. The MIT licence means it can be vendored into a closed-source product without obligation beyond keeping the notice, and for a package this small that is a realistic option if a supply-chain review makes you uncomfortable with the dependency.

One thing to check before depending on it is the engines field, which requires Node 12 or newer, and the fact that the package is ESM only, which is the more likely friction point in an existing CommonJS codebase.

Editorial conclusion

escape-goat suits a project that needs one specific thing, escaping and unescaping the five standard HTML entities, and wants it done by a two-function package with a tagged template form that makes accidental omission hard. It is the wrong tool if you need to escape for an attribute context or a URL, since the same escaping applied in a script tag opens the app to XSS as the README states, and it is the wrong tool if you need sanitisation, since escaping five characters is not a sanitiser. Verify first where your interpolated values land, in a text node, a quoted attribute, or a script body, and note that the package is at version 4.0.0 from 2021-04-16 with the last push on 2026-07-23 and no dependency of any kind.

Frequently asked questions

What does escape-goat escape?

htmlEscape escapes the characters & < > " and ' in the given string, and htmlUnescape reverses it by unescaping the entities &amp; &lt; &gt; &quot; and &#39;.

How do I install escape-goat?

Run npm install escape-goat. The package is ESM only with type set to module, so it is imported with import {htmlEscape, htmlUnescape} from 'escape-goat';.

Is escape-goat safe to use inside a script tag?

No. The README states that this escaping method is only safe when inserting data into normal tags like body, div, p, b and td, and that inserting htmlEscape'd data into tags like script and style opens your app to XSS vulnerabilities.

What are the tagged template literals in escape-goat for?

Each function also works as a tagged template literal that escapes or unescapes the interpolated values while leaving the static markup alone, which means a value cannot be interpolated without being escaped.

Why does escape-goat exist rather than using a sanitiser?

The README's FAQ says the author could not find a package that was tiny, well-tested and had both escape and unescape methods. Escaping preserves structure exactly while a sanitiser parses and rebuilds markup, removing anything it considers dangerous.

Does escape-goat have any dependencies?

No. The published files are index.js and index.d.ts, the package manifest lists no dependencies and only ava, tsd and xo as devDependencies, and the test script runs xo and ava and tsd together.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sindresorhus/escape-goat on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sindresorhus-escape-goat.svg)](https://hysenlabs.com/projects/sindresorhus-escape-goat)

Was this useful?