Installing Xposed in Android on Chrome OS

UPDATEAt present, for the latest Chrome OS versions (69, 70+), the following method seems to be the most reliable:

1. Ensure that your Android container is rooted, and that the rootfs is writable.

(If you've already used the rooting script, but you find the Android rootfs still isn't writeable, this may help).

2.  Download the Xposed framework installer app (to ~/Downloads).

3. Install the app via the Chrome OS shell (as root), e.g. :
sudo su -
setenforce 0
printf "pm install /storage/emulated/0/Download/XposedInstaller_3.1.5.apk" | android-sh 
printf reboot | android-sh
4. Assuming the installation was successful, open the Xposed installer app from the Chrome OS Launcher, click on 'Install/Update' then, in the next menu, click on 'Install'.

NB: If the prompt to grant root access doesn't show up at this point, see step 2. of the older instructions further down this page.

5. Once the framework installer app indicates it is 'Done', then reboot. 

The above steps should work on current Chrome OS versions. If you find it doesn't work for you, feel free to leave a comment on this post.

The original blog post is below...

Just a short post to note that it is now possible to successfully install and use Xposed in Android in Chrome OS. It works for me on CrOS version 67.0.3390.0 canary (arm), but it may be possible on some earlier builds, too.* 

I created a shell script that copies over Xposed's files some time ago, just as an experiment to see if it worked, but did not have success booting Android after modifying the rootfs with the script. I decided to take another look recently, and realised that it had a couple of bugs. Once those were fixed, I was able to successfully install Xposed with my script. 

 After success with a script based approach, I thought I'd try using the official installer app, and found that it actually isn't necessary to copy the files manually/with a script. Given a writeable Android rootfs, the installer app was able to install the framework successfully, as long as I allowed the app to gain root (which, on at least one CrOS version, necessitated temporarily setting 'Default Access' in SuperSU to grant root automatically, since the regular pop-up root request prompt didn't appear for this app). 

Here are the exact steps I took to install Xposed from its installer app.

1. After creating a rooted writeable Android rootfs with the rooting script, I downloaded the latest Xposed installer app from the Xposed announcement thread on xda.  

2. Next, I had to open the SuperSU app, go into its settings and temporarily set 'Default Access' to 'Grant'.

3.  After this, I simply clicked on 'Install/Update' in the Xposed installer, and then 'Install' again in the following menu. 

Following this, Xposed was installed, and all that was left to do now was reboot Android. To reboot Android without having to reboot Chrome OS, in a root CrOS shell, I entered 

printf reboot | android-sh

Just like on regular Android devices, the first boot after installing Xposed takes some time. To check on the progress (and to ensure that the Android container wasn't bootlooping, as had been the case when I'd previously attempted to install Xposed manually, without success, some time ago), I entered 

printf logcat  android-sh

into the CrOS root shell.  Happily, unlike my previous attempts in the past, this time I could see the boot proceeding as expected. 

After Android had loaded up, I went into the Xposed installer again, and added a few modules. Everything seems to be working as expected, as far as I can see, within certain limitations, mostly related to running in a container environment.

For example: adding shortcuts and actions to screen tap areas with 'Xposed Edge' works, but the shortcuts/actions only function when Android apps are actually on the screen. Also, the lack of 'parallel running of tasks' in the CrOS version I'm currently running could possibly be an issue.

In any case, now I actually have Xposed working, I can't seem to see much use for it at present. However, I guess it could possibly be useful for something in future.

*It seems not to be easily possible currently to install Xposed via the installer app on CrOS version 65 at present, due to a (temporary) issue mounting the Android rootfs as writeable at runtime. Installing files (manually, or via a script) into the container image while it is mounted elsewhere does still work. The writeable mount issue is also present on v66 but can be fixed by editing the container config.json (deleting the string "dev", from the "rootfs/root" section under "mounts". 

Modifying hardware key layouts for Android apps on Chrome OS

I recently used an Android terminal emulator on Chrome OS for the first time in a while, and encountered the following issue: although my locale/keyboard settings in CrOS were set to UK, it seemed to be the case that certain Android apps, e.g. terminal emulators were stuck with the US keyboard mappings. Due to this, I found myself mistyping certain characters (mainly @ and "), when switching back and forth between the Chrome OS shell and Termux/Terminal Emulator. 

To fix the keylayout for the locale of my Chromebook's physical keyboard, I modified a couple of files on the rootfs. Although I edited the files directly, which requires a writeable Android system, it's also possible to make a copy, edit the copy, then bind mount it over the original.

Switching the " and @

First way - editing the file directly. Modified (R/W) Android rootfs needed.

With a read/write Android filesystem, as created with the rooting scripts, the easiest way to fix the key layout seemed to be by editing the relevant key mapping file directly. It would be best to make a backup of the file to be edited beforehand.

I opened a root shell in CrOS and entered
vi /opt/google/containers/android/rootfs/root/system/usr/keychars/Generic.kcm
(To swap the " and @ characters, in vi I entered /@ to search Generic.kcm for the @ character, pressed x to delete the @, then pressed i to enter "insert text mode" and typed " to add the quote mark character at the place where the @ had been. I did similar with the " character, replacing it with @)

After modifying the file I saved and quit vi by typing :wq!

I then rebooted the Android container
printf reboot | android-sh
Now, in Termux etc., Shift+2 types the " character, and Shift+' types the @ character, as is expected with a UK keyboard layout. 

Second way - Editing a copy of the file and bind mounting it over the original.

It's also possible to do it without editing the file directly, by copying it out to /usr/local (or to a place accessible within the File Browser such as ~/Downloads) editing the copy, bind mounting it back in place, then rebooting the Android container. In this case, the bind mount and Android reboot have to be redone after every time Chrome OS is rebooted.



To copy the file to /usr/local and edit it with vi, then bind it back: 

Copy the file with permissions preserved
cp -a /opt/google/containers/android/rootfs/root/system/usr/keychars/Generic.kcm /usr/local
Edit the copy e.g. in vi (as above).

Bind mount the file back over the original
mount --bind /usr/local/keychars/generic.kcm /opt/google/containers/android/rootfs/root/system/usr/keychars/Generic.kcm
Then reboot the Android container so it sees the new file
printf reboot | android-sh

Alternatively, it might be easier to copy the file to ~/Downloads and edit it with another text editor (e.g. Caret), then move it to /usr/local and bind mount it & reboot Android

Copy the file to ~/Downloads
cp -a /opt/google/containers/android/rootfs/root/system/usr/keychars/Generic.kcm /home/chronos/user/Downloads
After editing the file in text editor, move it to /usr/local/
mv /home/chronos/user/Downloads/Generic.kcm /usr/local/Generic.kcm
Mount bind and then reboot Android as above

Adding the #~ key found on UK keyboards

After swapping the " and @, I still had an issue with the key for # and ~ (located to the left of the Enter key on my UK keyboard). It appeared to have the same mapping in Android as the \ key (to the right of my left Shift key). It turned out that to fix this I had to do two things:

Step 1. Changing key 43 in cros_ec.kl

I found that key 43 and key 86 were both mapped to BACKSLASH at /system/usr/keylayout/cros_ec.kl in the Android container. I had a look at the list of key codes for Android, and decided to change key 43 from BACKSLASH to POUND, since the key code POUND in Android apparently refers to the # key found on phones.

Step 2. Changing the relevant key declaration in Generic.kcm  

then opened the key character map file I'd previously modified (/system/usr/keychars/Generic.kcm), found the key declaration for POUND therein, which looked like this:

key POUND {
    label:                              '#'
    base:                              '#'

And added a line for the shift modifier.

key POUND {
    label:                              '#'
    base:                              '#'
    shift:                               '~'

 I rebooted Android again, and now my keyboard was finally behaving how I wanted it to in Android terminal apps. 

If anyone is considering making the changes detailed above, or similar, I should mention that Android is quite particular about how it expects these files to be; if the formatting is at all out of place or if an unexpected character is present then Android might fail to load, or revert to a fallback character mapping. If the latter is happening then logcat usually notes the offending line in the problematic file, which can be seen in the output to the following (entered in a CrOS root shell):

printf "logcat | grep key" | android-sh
In  the event of Android failing to load completely and the above command returning an error, if the files had been bind mounted, a reboot would fix it, and if they were edited directly it might be necessary to restore the original file/s from backups.

Enabling Managed Profile in Android in Chrome OS

Sometimes it's useful to be able to run two copies of an app simultaneously in Android, e.g. for testing purposes, or to have two separate logins with an app which doesn't usually support this functionality. 

This can usually be done via the excellent app Island, which utilizes the Work/Managed profile functionality in Android in order to 'clone' selected apps, which can then be used either independently or both at the same time.

I wanted to have this functionality in Android on Chrome OS, but I found that initially the Island app didn't seem to be able to install properly.

I modified the file /system/etc/permissions/cheets.xml within the Android container, removing the word "unavailable" from two relevant lines, so

<unavailable-feature name="" />
<unavailable-feature name="" />


<feature name="" />
<feature name="" />

After rebooting Android, I was able to install Island fully, and was able to run it as normal, cloning apps, then keeping two copies of the same app open at the same time.

Since the Android file /system/etc/permissions/cheets.xml is located within the Android container, a writeable Android rootfs is needed in order to modify the file directly. One way of achieving this is via the rooting scripts.

Alternatively, for example if one perhaps wants just to run a quick test for a cloned app, the original rootfs can be kept untouched, and an edited cheets.xml file bind mounted over the original. However, the bind mount would only last until the Chromebook is rebooted, which could lead to unexpected consequences.


Copy cheets xml to /usr/local
 cp /opt/google/containers/android/rootfs/root/system/etc/permissions/cheets.xml /usr/local/cheets.xml   
Remove the word "unavailable-" from the device_admin line
sed -i 's/unavailable-feature name=""/feature name=""/g' /usr/local/cheets.xml
Remove the word "unavailable-" from the managed_users line
sed -i 's/unavailable-feature name=""/feature name=""/g' /usr/local/cheets.xml
Bind mount the edited cheets.xml back over the original
mount -o bind /usr/local/cheets.xml /opt/google/containers/android/rootfs/root/system/etc/permissions/cheets.xml
Finally, reboot the android container
printf reboot | android-sh
I should note that, although I haven't noticed any issues as yet, enabling unsupported features may have unexpected consequences. It would be prudent to ensure any important files are backed up before experimenting.

Enabling Mock Locations in Android on Chrome OS

On a Chromebook in Developer Mode, it appears to be possible to successfully enable mock locations for Android via the command prompt. I tested the method in Terminal Emulator (which requires root in Android), but it should also be possible to do in the Chrome OS shell via adb.

In Developer mode, with adb debugging switched on in the Android Developer options, if we connect to Android via adb in the Chrome OS shell
sudo su -
adb connect
We can then switch on mock locations via adb
adb shell settings put secure mock_location 1
If a suitable fake location Android app (e.g. is installed on the device, this can be set as the default mock locations app
adb shell appops set com.lexa.fakegps android:mock_location allow  
To set a different app as the mock locations app, change the package name/App ID (com.lexa.fakegps in the example above) to the appropriate package name (as shown on the app's Play Store URL).

After setting Fake GPS as the mock locations app, I was able to select my mock location in Fake GPS and e.g. Maps in Opera would successfully show the mock location, rather than my current location. I haven't really tested in many other apps at this time. 

I understand that some Android apps may specifically check if mock locations are being used. For those apps, I imagine further steps would probably be required, such as perhaps installing (possibly a different app such as GPS JoyStick) as a system app. This, however, would require remaking the Android rootfs container as a R/W image. If this is required, the rooting script can be run, which, as part of the rooting process, replaces the Android read-only rootfs with a R/W copy.

Enabling third party input methods in Android on Chrome OS

At the moment, the default choice of input methods for Android apps in Chrome OS seems to be limited to either the physical keyboard, or the built-in Chrome OS virtual keyboard. Installing a third party Android IME and clicking on its 'Enable' button (which, on Android phones and tablets, usually brings up the Languages and Input dialog where third party IMEs can be enabled), brings up CrOS' own 'Languages and Input' settings dialog, usually found at chrome://settings/languages, rendering it apparently impossible to choose a third party input method for Android apps this way.

However, it is possible to enable third party input methods via adb in the shell, or with the Android terminal. Once the third party input method has been added to the list of enabled Android input methods, it can be selected as the default input method for Android apps.

I already had 'Hacker's Keyboard' installed on the Android instance but had not been able to enable it, as it didn't seem to be possible to open up the Android 'Languages and Input' settings dialog. 

One thing I tried first of all was editing /system/etc/permissions/cheets.xml as in that file I noticed this:

   <!-- Disallow third-party IME apps in favor of ARC IME. -->
    <unavailable-feature name="" />

However, it turns out that editing this isn't necessary.

I was able to enable IMEs via the android shell 'settings' command, either in the Android terminal emulator (which requires root), or through Chrome OS via adb, which does not require a rooted Android instance.

Here's how I did the latter:

1. Enabled ADB debugging in the Android Developer options

2. Opened Chrome OS shell (Ctrl_Alt+T, type shell), and connected to the Android instance with
adb connect
3. Agreed to the RSA authentication dialog popup (ticked the box).

4. To check currently enabled input methods I entered
adb shell settings get secure enabled_input_methods
which returned: org.chromium.arc.ime/.ArcInputMethodService

5. Added HK to the list of enabled input methods by appending its ID to the latter string, i.e. entering
adb shell settings put secure enabled_input_methods org.chromium.arc.ime/.ArcInputMethodService:org.pocketworkstation.pckeyboard/.LatinIME
Edit: See the footnote at the bottom of this page for an alternate, perhaps easier, way to do this, and how to find the IDs for installed input methods

6. To set a new default, I opened the Hacker's Keyboard app via the search button/launcher,`and tapped 'Set input method', which brought up the Android IME picker, where HK was now visible. Picked it as the default, and hit the "show keyboard when a physical keyboard is active" switch and it appeared!

To set the IME as default via adb: Rather than having to find the Android IME picker - after adding the ID of the input method to the 'enabled' list, I could have set it as the default with
adb shell settings put secure default_input_method [ID]
e.g. in this case:
adb shell settings put secure default_input_method org.pocketworkstation.pckeyboard/.LatinIME

7. Done! Here's what it looks like in Firefox in tablet mode:

Caution is advised if enabling third party Android input methods this way - I found that HK mostly seemed to function as expected in tablet mode, working perfectly with some Android apps that I tried such as Firefox, e.g. popping up when a text input box was highlighted, then disappearing when I tapped away from the text input box allowing me to navigate by touch normally by dragging to scroll, tapping links, etc, and reappearing if an input box was highlighted again. 

In other apps, however, e.g. Terminal Emulator, the interaction seemed perhaps a little buggy; scrolling initially didn't work (but started working after I did a few things (fiddled with the 'Window Size and Orientation' settings in Android's 'Developer Options/maximised HKs parent app/rebooted), and after closing the keyboard, it didn't immediately pop up again when tapping in an input field.

Due to the latter issue, I found it necessary to customize the settings a little; in the Chrome OS shell, I entered
adb shell am start -n org.pocketworkstation.pckeyboard/.PrefScreenActions
which brought up HKs 'Gesture and key actions' preference page. For the gesture 'Swipe Left', I chose 'Launch Settings', and for the gesture 'Swipe Right', I chose 'Close keyboard'. I then went back to the Hacker's Keyboard app and disabled extra languages for ease of swiping left/right over the spacebar.

Finally, I opened up the keyboard's main settings dialog by swiping left over the spacebar area, and ticked the 'Use permanent notification' box so that, should it not pop up automatically in any Android apps, it may still be activated simply by tapping the notification.

Rather than setting up a swipe gesture, for the purpose of opening the settings dialog to enable the permanent notification, I could instead have entered
adb shell am start -n org.pocketworkstation.pckeyboard/.LatinIMESettings
to bring Hacker's Keyboard's main settings dialog activity up.

I also tried installing Gboard from the Play Store. Again, it can be chosen as a default input method for Android apps, once its string has been added to enabled_input_methods:

e.g. to have Gboard and the original CrOS virtual keyboard enabled and available to be chosen as default:
adb shell settings put secure enabled_input_methods org.chromium.arc.ime/
or to have both Hacker's Keyboard and Gboard enabled, in addition to the original CrOS virtual keyboard:
adb shell settings put secure enabled_input_methods org.chromium.arc.ime/.ArcInputMethodService:org.pocketworkstation.pckeyboard/
Gboard worked OK but was not perfect; sometimes taking over the screen - the lack of a 'back' button in tablet mode being something of an issue (swiping down not seeming to work to disable it). Edit: Although a 'back' button has now been implemented in CrOS in tablet mode, it is located in the shelf, which now seems to be getting pinned to the bottom of the screen, behind the keyboard.

I also tried out SwiftKey:
adb shell settings put secure enabled_input_methods org.chromium.arc.ime/.ArcInputMethodService:com.touchtype.swiftkey/com.touchtype.KeyboardService
This seemed to work quite well in portrait mode, but I did encounter some issues with graphics glitches in landscape mode.

Footnote: the 'ime' command (perhaps an easier method)

The above was what I did initially but it's not the only way. Similarly, it is possible, and perhaps easier, to enable input methods with the 'ime' command.

Installed input methods and their associated IDs (among other information) can be listed with
adb shell ime list -a
The IDs of currently enabled input methods can be listed with
adb shell ime list -s
An input method can be enabled with
adb shell ime enable [ID]
And an input method can be set as the default with
adb shell ime set [ID]

Running a shell command with a single click on a shelf shortcut on Chrome OS

I wanted to find a way to execute a shell command on a Chromebook via a single click on a bookmarked icon in the shelf. After a bit of research, I discovered a way to do it. It involves setting up the Chromebook as a ssh host, which is perhaps overkill, and could potentially have security implications, but also means I can access the chromebook's shell via ssh from my Android phone/tablet (or any other computer), which could be useful some day.

Here's how I did it on a freshly powerwashed device (in Developer mode). Not all of the below steps are strictly necessary; as ever, YMMV.

Briefly, what I did was: 

Installed Secure Shell extension; setup sshd on the Chromebook; added public key from ConnectBot (Android app) to ~/.ssh/authorized_keys in order to test connection; created profile to connect to chronos@localhost/my local IP address in Secure Shell extension; generated keys on Chromebook (in  ~/.ssh); added generated public key to ~/.ssh/authorized_keys; imported newly-generated private/public key pair within Secure Shell app (one by one); created bookmark link to chronos@localhost profile in shelf, and, finally, added required command to 'arguments' field of profile in Secure Shell.

A more detailed description follows.

1. Installed Secure Shell extension

Installed Secure Shell extension from

2. (a) Added ssh upstart job to /etc/init/

Entered the following in the shell:

sudo ln -s /usr/share/chromeos-ssh-config/init/openssh-server.conf /etc/init/openssh-server.conf

The above file, openssh-server.conf, will auto-start sshd at boot, and, additionally, will add a firewall rule which opens port 22, enabling ssh access to the device from external networks. This firewall rule is only required if connecting from another device - to use ssh purely within the Chromebook, it is not necessary to open port 22. Therefore, if not intending to connect to the Chromebook from another device via ssh, one may create a .conf in /etc/init omitting this part. For instance, for purely accessing localhost, the following works for me:

sudo su -
echo "start on starting system-services


exec /usr/sbin/sshd

end script" > /etc/init/openssh-server.conf 
chmod 644 /etc/init/openssh-server.conf

2. (b) Registered and initialised upstart job

sudo initctl reload-configuration
sudo initctl start openssh-server 

The above pair of shell commands registered and then initialised the open ssh server. After entering the commands, the server was running in the background.

3. (a) Optional: Tested the connection with ConnectBot on Android tablet (Nexus 9)

This step is not actually necessary (unless you specifically want to control your Chromebook in the shell with an Android device).

Rebooted Chrome OS, launched ConnectBot, generated Pubkey, long-pressed on newly-generated key, chose Copy public key. Mailed it to self. Opened Gmail on the Chromebook, copied key from email, opened shell tab (non-root) and added the key to Chrome OS as follows:

(*DOUBLETAP* indicates a double tap on the touchpad to paste the key)

touch ~/.ssh/authorized_keys
echo *DOUBLETAP* > ~/.ssh/authorized_keys
chmod 600  ~/.ssh/authorized_keys

Created a new host profile in ConnectBot; set the second field as chronos@ (my local IP address); left all other fields as default.

Connected to the profile...

chronos@localhost / $

Success! Disconnected ConnectBot. Back in the host profile, edited the field "Post-login automation". to add a shell command:


Connected to the profile...

The Chromebook sleeps!

Long pressed the N9 homescreen to bring up the "Add Widget" dialog. Dragged ConnectBot to homescreen. Chose chronos@ from the pop-up list of hosts. Tapped the new homescreen shortcut...

The Chromebook sleeps!

Unfortunately, the current implementation of Android on Chrome OS doesn't appear to support  creating homescreen 'widget shortcuts'. So it seemed that another approach was required in order to create the one-click shortcut.

3. (b) Connected to the Chromebook... from the Chromebook

Opened Secure Shell extension in Chrome with the keyboard shortcut Ctrl+Alt+T, typed ssh, in omnibar, hit Tab, hit Enter.

Clicked on "New Connection"

Typed chronos@localhost

Clicked on "Connect"

At this point, it only seemed to be possible to connect after setting a password for chronos (passwd chronos), and then the password had to be entered manually at every connection. No good for automation. 

Some further setup seemed to be required.

Generated a new key, added the generated key to ~/.ssh/authorized_keys, and copied the keys to ~/Downloads.

cd ~/.ssh/
echo | ssh-keygen -P ''
cat ~/.ssh/ >> ~/.ssh/authorized_keys
cp -a ~/.ssh/id_rsa* ~/Downloads

Opened Secure Shell

Chose 'Import'

Navigated to private key in the file explorer (/home/chronos/user/Downloads/id_rsa). Clicked on it.

Chose 'Import', again

Navigated to public key in the file explorer (/home/chronos/user/Downloads/ Clicked on it.

Tried connecting to the chronos@localhost profile in Secure Shell again and... Boom!

chronos@localhost / $

4. Bookmarked the extension

With the Secure Shell profile command prompt tab open (which looks identical to the regular shell command prompt tab, except that in the address bar, rather than ending with crosh.html, the address ends with a profile number):

Three dot menu at top right>More tools>Add to shelf (open as window)

This created a clickable shelf bookmark icon.

5. Added the shell command

So now it was possible to open a shell command prompt with a single click, the final part of the puzzle was to add the command of interest. This turned out to be a piece of cake, once the syntax was correct. All that needed to be added to the saved Secure Shell profile was 

-- thecommand the SSH Arguments field.

So, after adding 

-- powerd_dbus_suspend

to the SSH Arguments field for my chronos@localhost profile, the final result is a bookmark in the shelf which, when clicked, sends the Chromebook to sleep. Since I added it to the 9th position on the shelf, it can also be triggered with the keyboard shortcut: Alt+9.

I am not sure if it will be possible to set it up so that the command runs without opening a window, but it does at least seem to remember the window size and position, so the window can be hidden away at the side, or rolled up' into a small bar. I think it might be possible to set it up so the command runs in the background but I haven't looked into this yet.

Other use cases: Crouton etc.

Over on xda, user DandyRandyMarsh posted that he was able to successfully use this procedure to set up a shelf shortcut to open single apps within a crouton chroot in a window, simply by following the above and adding the relevant crouton command to the SSH Arguments field. He shared his example, a shortcut to enter steam;

-- sudo enter-chroot xiwi steam

One thing to bear in mind with this is that if your setup requires a password to sudo, you may need to change the command slightly. I haven't tested it out too much, but in my case, with a password prompt for sudo and a chroot on an external drive, I can open Firefox within my Ubuntu chroot in a xiwi window with a shelf shortcut after adding 

-t -- echo mypassword | sudo -S sh /media/removable/3/bin/enter-chroot xiwi firefox

to the Arguments field.

Similarly, I set up a shelf shortcut to open my chrooted Ubuntu desktop in a window with the argument

-t -- echo mypassword | sudo -S sh /media/removable/3/bin/startxfce4

Another way to do it is to save the string to a file in /usr/bin, then just put -- thefile as the argument. This is what I did with a few commands that I find it tedious to regularly open up a prompt and enter in, such as the ectool command(s) to switch off the bright LEDs on the side of the device:

sudo ectool led power off
sudo ectool led battery off

Customizing the icons

Of course, no-one wants a bunch of generic black squares darkening their desktop, so, in order to customize the shelf icon for these shortcuts, a few extra steps are necessary. 

Although the trick of injecting a favicon via js only seems to work for websites, and customizing the icon for Secure Shell itself might be pretty pointless as it'd be the same for all shortcuts (and get lost after the extension updates), we can set custom icons for our shell shortcuts as follows:

When you add shortcuts to the shelf on Chrome OS like this, a minimal app is created for each one, with its own manifest.json, and icon.pngs. Initially, the shell shortcuts were getting created with blank pngs as the icons by default (appearing as a black box with P in the center). 

So, all we need to do, is get some icons, find the right directory for our shelf shortcut app, then copy our new icons into it...

For instance, here's how I made a nice shiny icon for my single app shelf shortcut to Firefox.

(a) Prepared icons

Created a temporary directory in ~/Downloads to store my icons. 

Prepared a set of six icons in ~/Downloads/firefox. (I downloaded mine from 
To avoid any manifest editing, saved them as *size*.png

e.g. 32.png; 48.png; 64.png; 96.png; 128.png; 256.png

(b) Located correct directory to copy icons into; copied icons into folder:

Easy way:

As long as a somewhat unique name was chosen for the shelf shortcut, the easiest way to locate its directory is probably

cd ~/Extensions
grep -r thenameoftheshelfshortcut

Navigate to the Icons subdir, check its contents and copy over the new icons i.e. in my case:

cd thenameoftheshelfshortcut
cd */icons
sudo cp ~/Downloads/firefox/* .

Other way:

If a generic name was chosen, and grepping it brings up too many results to go through,
another way to locate the correct directory is as follows:

(It is probably easiest to do this directly after clicking "add to shelf")

To change directory into the most recently modified extension icon subfolder automatically, the following works for me. 

cd ~/Extensions/&&cd $(ls -v1td */ | sed '/\Temp/d' | head -1)/&& cd $(ls -v1td */ | head -1)/icons&&ls -al
If the above one-liner worked correctly, a list of blank icons (*.png) should be visible. The file creation time/dates should be checked, and if they match the time the shortcut was added to the shelf, the directory is the right one, and new icons can simply be copied over these, e.g.

sudo cp ~/Downloads/firefox/* .

If the one-liner didn't work;  the icons directory can be located manually i.e.

ls -ltr ~/Extensions 

The most recently added folder is at the bottom. Then, 

cd themostrecentlyaddedfolder

(or, the folder modified at the date/time that the shortcut was "added to shelf"). Then,

cd */icons
sudo cp ~/Downloads/firefox/* .

I repeated the above with a set of Ubuntu icons for my startxfce4 shortcut, and a few other Linux programs I use via crouton, for good measure.

Then just did sudo restart ui, and enjoyed the shiny new shelf icons!

I hope that these changes will get picked up by CrOS's extension sync, thus getting backed-up and restored automatically. At this point I am not sure if that will be the case, though.

Update regarding the latter: After an OS update, the only part that needed redoing was the SSH upstart job. After a powerwash, however, in addition to having to generate keys and add them to Secure Shell again, while the extension shortcuts (and Secure Shell profiles) themselves synced and restored OK , the icons did not. Next time, I'll see if adding a favicon URL instead of icon files works at all, and, if not, to restore them, I suppose I might have to add this into the customization scripts I run post-powerwash.