--- layout: post title: "Firefox custom search engine - search.json.mozlz4" date: 2025-05-03 04:16:49 +0100 --- So I have an ancient custom search engine XML that was broken by remote API changes. In trying to fix it, I inadvertently had it deleted from search.json.mozlz4, and have to figure out how to add it back. This is basically a rewrite of [this post by Frederick Zhang](https://blog.onee3.org/2018/04/manually-add-a-search-engine-to-firefox-quantum/). That post is excellent, but there have been some changes since 2018. The following is tested on the latest stable Firefox (138.0.1). First download [this Python script](https://gist.github.com/kaefer3000/73febe1eec898cd50ce4de1af79a332a/a266410033455d6b4af515d7a9d34f5afd35beec). Use it to decompress search.json.mozlz4 (located in the Firefox profile directory): `python3 mozlz4a.py -d search.json.mozlz4 search.json` Optionally, format it with `python3 -m json.tool`. Add a new object to the `"engines"` array. My example for Startpage with custom params: ``` { "id": "38c37483-6e61-4f86-bfaf-2b99ed7d8464", "_name": "Startpage (Unfiltered)", "_loadPath": "[profile]/searchplugins/startpage-unfiltered.xml", "_iconMapObj": { "16": "data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2jkj+9YtD/vWLQ/71i0P+9otD/vaLRP72i0T+9YtE/vWLRP72i0T+9otD/vaNRP72jUT+9otF/vaLRf73kkv+9Yc///WJP//1iT//9Yk///rAmf/94Mz/+sCa//aRTv/1iUH/9ok///aJP//2i0H/9otB//aJQv/2iUL/9otC//aNRP/2jUT/9o1E//aNRP/6wpv////////////96dr/95dQ//aNRP/2kET/9pBG//aQRv/2kEb/9pBG//aRR//3lEz/95BH//mueP/7xJ3/959g//efYf/4p23//vDm//3p2//3kEr/95FJ//aRSf/niFH/95FK//aRSv/2mE//95hS/vq4iP/////////////////81bj/95xZ//q4iP//////+bF+//eZT//njFT/PSqi/2xGjv/2mVD/951V/vedVv783cX///////vQrf/++PP///////748//+8uj///////m3gf/olFr/PSuj/w8Pt/9sSJD/951V//eeWf73oVv++8ul///////5sXf/+KRi//vRsf////////////3r3v/olF//Piyk/w8Pt/9sSJH/+J5Z//ieWv/3oV/++KZf/vihXP/97N7//vn0//zTs//6wJP/+bBy//q6iP/onW//Piyl/w8Pt/8fGbH/m2iB/+icY//4pGD/96hl/viqZf74pmD/+Kxr//3iy/////////n1//ivbP/onGj/Pi2m/w8Pt/8uJKz/fFeQ/x8Zsf8+Lqb/6J9r//ivbP74rm3++Klm//mpZv/5q2f/+bR9//m0e//poW7/Pi6n/w8Pt/9sTZj/+Ktp//ira/+rd4P/Dw+3/4xijv/5snH++LN1/vmvbf/5r23/+a5t//mvb//4r2//TTuk/w8Pt/8fGrL/6ah1//ivcP/4r3P/q3yI/w8Pt/+MZpP/+bN5/vm4ev75t3X/+bV1//m1df/5t3X/+Ld3/8qUhP98XZn/Hxqz/+mse//5t3f/2p+B/x8as/8PD7f/u4qK//m7fv76u4D++bl7//m3fP/5uXz/+bl8//m5fP/5t3z/+bl//x8as/9NPKf/fWCb/x8as/8PD7f/bVOh//q5f//6v4X++sGI/vm9g//5voX/+b6F//m9hf/6vYX/+r6F//nCh/+bepr/Hxu0/w8Pt/8PD7f/fWOh//q+hf/6wof/+saN/vrGjf75xIv/+ceL//nEi//5xIv/+sSL//rHi//6x43/+ceN/+m7kP+7lpj/6ruQ//rHkP/6x43/+seQ//rLlf76ypT++seR//rJkf/6yZH/+seR//rJkf/6yZH/+8mR//vJlP/7yZT/+smU//rJlP/6yZT/+8yV//rJlf/6zpn+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, "_metaData": { "loadPathHash": "", "alias": "sp", "hideOneOffButton": false, "order": 8 }, "_urls": [ { "params": [ { "name": "query", "value": "{searchTerms}" }, { "name": "cat", "value": "web" }, { "name": "lui", "value": "english" }, { "name": "prfe", "value": "c774daad5dd23ae2db70252aa321b9354508577d11ee4a79664d62c5d56672dd946c593d0daa5e9f3e7d1ebafaf773669d191e7b802605a743232b31a2254feeccf8ccc0f306bf45f8fa73f3" } ], "rels": [], "template": "https://www.startpage.com/sp/search", "method": "POST" } ], "_orderHint": null, "_telemetryId": null, "_filePath": "/searchplugins/startpage-unfiltered.xml", "_definedAliases": [], "_updateInterval": null, "_updateURL": null } ``` I assume the `"id"` is just some UUID. I ended up reusing an existing one that I no longer needed. I'm not sure whether it would be "validated" in any way. The `"loadPathHash"` is calculated using the following function: ``` function getVerificationHash(name, profileDir = PathUtils.profileDir) { let disclaimer = "By modifying this file, I agree that I am doing so " + "only within $appName itself, using official, user-driven search " + "engine selection processes, and in a way which does not circumvent " + "user consent. I acknowledge that any attempt to change this file " + "from outside of $appName is a malicious act, and will be responded " + "to accordingly."; let salt = PathUtils.filename(profileDir) + name + disclaimer.replace(/\$appName/g, Services.appinfo.name); let data = new TextEncoder().encode(salt); let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); hasher.init(hasher.SHA256); hasher.update(data, data.length); return hasher.finish(true); } ``` This code is taken from the Firefox source code [here](https://searchfox.org/mozilla-central/rev/4c065f1df299065c305fb48b36cdae571a43d97c/toolkit/components/search/SearchUtils.sys.mjs). It is to be pasted into the Browser Console, then called with `getVerificationHash("", "")`. The second argument is required if you are running on a different profile. The `"order"` is just whichever unused number that comes next. Save this, then compress it again with: `python3 mozlz4a.py search.json search.json.mozlz4`. Finally, create a backup of the original search.json.mozlz4, and replace it with the new one.